@prajwolkc/stk 0.7.0 → 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.
@@ -8,1803 +8,22 @@
8
8
  */
9
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
- import { z } from "zod";
12
- import { loadConfig, enabledServices } from "../lib/config.js";
13
- import { getChecker, allCheckerNames, loadPluginCheckers } from "../services/registry.js";
14
- import { getLocalBrainClient, ingestProject, loadBrainStore, saveBrainStore, syncBrain, pushToCloud, pullFromCloud, brainCheck, brainDiagnose, reviewDiff } from "../services/brain.js";
15
- import { recordMetric, getMetrics, compareToBaseline, getDeployFrequency, getErrorRate, getUptime } from "../services/metrics.js";
16
- import { runAllChecks } from "../services/security.js";
17
- import { execSync } from "child_process";
11
+ import { registerInfraTools } from "./tools/infra.js";
12
+ import { registerOpsTools } from "./tools/ops.js";
13
+ import { registerDataTools } from "./tools/data.js";
14
+ import { registerBrainTools } from "./tools/brain.js";
15
+ import { registerGithubTools } from "./tools/github.js";
16
+ import { registerSecurityTools } from "./tools/security.js";
18
17
  const server = new McpServer({
19
18
  name: "stk",
20
- version: "0.2.0",
21
- });
22
- // ──────────────────────────────────────────
23
- // Tool: stk_health
24
- // ──────────────────────────────────────────
25
- 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.", {
26
- all: z.boolean().optional().describe("Check all known services, not just configured ones"),
27
- }, async ({ all }) => {
28
- await loadPluginCheckers();
29
- const config = loadConfig();
30
- const serviceList = all ? allCheckerNames() : enabledServices(config);
31
- const checks = serviceList.map(async (name) => {
32
- const checker = getChecker(name);
33
- if (!checker) {
34
- return { name, status: "skipped", detail: `unknown service "${name}"` };
35
- }
36
- return checker();
37
- });
38
- const results = await Promise.all(checks);
39
- const down = results.filter((r) => r.status === "down");
40
- return {
41
- content: [
42
- {
43
- type: "text",
44
- text: JSON.stringify({
45
- project: config.name,
46
- services: results,
47
- summary: {
48
- healthy: results.filter((r) => r.status === "healthy").length,
49
- down: down.length,
50
- skipped: results.filter((r) => r.status === "skipped").length,
51
- total: results.length,
52
- },
53
- ok: down.length === 0,
54
- }, null, 2),
55
- },
56
- ],
57
- };
58
- });
59
- // ──────────────────────────────────────────
60
- // Tool: stk_status
61
- // ──────────────────────────────────────────
62
- server.tool("stk_status", "Get a complete status overview: git state, service health, last deploy, and open issues — everything in one call.", {}, async () => {
63
- const config = loadConfig();
64
- const status = { project: config.name };
65
- // Git
66
- try {
67
- status.git = {
68
- branch: execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim(),
69
- dirty: execSync("git status --porcelain", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().split("\n").filter(Boolean).length,
70
- lastCommit: execSync('git log -1 --format="%s"', { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim(),
71
- lastCommitAge: execSync('git log -1 --format="%cr"', { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim(),
72
- };
73
- }
74
- catch {
75
- status.git = null;
76
- }
77
- // Services
78
- await loadPluginCheckers();
79
- const serviceList = enabledServices(config);
80
- if (serviceList.length > 0) {
81
- const checks = serviceList.map(async (name) => {
82
- const checker = getChecker(name);
83
- if (!checker)
84
- return { name, status: "skipped" };
85
- return checker();
86
- });
87
- const results = await Promise.all(checks);
88
- status.services = {
89
- healthy: results.filter((r) => r.status === "healthy").length,
90
- down: results.filter((r) => r.status === "down").map((r) => r.name),
91
- skipped: results.filter((r) => r.status === "skipped").length,
92
- total: results.length,
93
- };
94
- }
95
- else {
96
- status.services = { total: 0, note: "no services configured" };
97
- }
98
- // Deploy
99
- if (process.env.VERCEL_TOKEN) {
100
- try {
101
- const res = await fetch("https://api.vercel.com/v6/deployments?limit=1", {
102
- headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
103
- });
104
- const data = (await res.json());
105
- const dep = data.deployments?.[0];
106
- if (dep) {
107
- status.lastDeploy = {
108
- provider: "vercel",
109
- state: dep.readyState ?? dep.state,
110
- url: dep.url,
111
- created: dep.created,
112
- };
113
- }
114
- }
115
- catch { /* skip */ }
116
- }
117
- return {
118
- content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
119
- };
120
- });
121
- // ──────────────────────────────────────────
122
- // Tool: stk_doctor
123
- // ──────────────────────────────────────────
124
- server.tool("stk_doctor", "Diagnose infrastructure configuration issues. Checks for missing env vars, mismatched config, invalid URLs, and suggests fixes with documentation links.", {}, async () => {
125
- const config = loadConfig();
126
- const enabled = enabledServices(config);
127
- const issues = [];
128
- const ENV_REQS = {
129
- railway: { required: ["RAILWAY_API_TOKEN"], optional: ["RAILWAY_PROJECT_ID", "RAILWAY_ENVIRONMENT_ID", "RAILWAY_SERVICE_ID"] },
130
- vercel: { required: ["VERCEL_TOKEN"], optional: ["VERCEL_PROJECT_ID"] },
131
- fly: { required: ["FLY_API_TOKEN"], optional: ["FLY_APP_NAME"] },
132
- render: { required: ["RENDER_API_KEY"], optional: [] },
133
- aws: { required: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"], optional: ["AWS_REGION"] },
134
- database: { required: ["DATABASE_URL"], optional: [] },
135
- mongodb: { required: ["MONGODB_URL"], optional: [] },
136
- redis: { required: ["REDIS_URL"], optional: [] },
137
- supabase: { required: ["SUPABASE_URL"], optional: ["SUPABASE_SERVICE_KEY"] },
138
- r2: { required: ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN"], optional: [] },
139
- stripe: { required: ["STRIPE_SECRET_KEY"], optional: [] },
140
- };
141
- for (const svc of enabled) {
142
- const reqs = ENV_REQS[svc];
143
- if (!reqs)
144
- continue;
145
- const missingReq = reqs.required.filter((v) => !process.env[v]);
146
- const missingOpt = reqs.optional.filter((v) => !process.env[v]);
147
- if (missingReq.length > 0) {
148
- issues.push({ level: "error", service: svc, message: `Missing required: ${missingReq.join(", ")}` });
149
- }
150
- else {
151
- issues.push({ level: "ok", service: svc, message: "Configured correctly" });
152
- }
153
- if (missingOpt.length > 0) {
154
- issues.push({ level: "warn", service: svc, message: `Missing optional: ${missingOpt.join(", ")}`, fix: "Needed for logs, env sync, deploy watching" });
155
- }
156
- }
157
- return {
158
- content: [{
159
- type: "text",
160
- text: JSON.stringify({
161
- project: config.name,
162
- issues,
163
- summary: {
164
- errors: issues.filter((i) => i.level === "error").length,
165
- warnings: issues.filter((i) => i.level === "warn").length,
166
- ok: issues.filter((i) => i.level === "ok").length,
167
- },
168
- }, null, 2),
169
- }],
170
- };
171
- });
172
- // ──────────────────────────────────────────
173
- // Tool: stk_logs
174
- // ──────────────────────────────────────────
175
- server.tool("stk_logs", "Fetch recent production logs from Railway, Vercel, or other deploy providers. Useful for diagnosing errors and understanding runtime behavior.", {
176
- provider: z.enum(["railway", "vercel"]).optional().describe("Which provider to fetch logs from (auto-detects if omitted)"),
177
- lines: z.number().optional().default(30).describe("Number of log lines to fetch"),
178
- }, async ({ provider, lines }) => {
179
- // Railway logs
180
- if ((provider === "railway" || !provider) && process.env.RAILWAY_API_TOKEN) {
181
- const token = process.env.RAILWAY_API_TOKEN;
182
- const projectId = process.env.RAILWAY_PROJECT_ID;
183
- const serviceId = process.env.RAILWAY_SERVICE_ID;
184
- if (!projectId) {
185
- return { content: [{ type: "text", text: JSON.stringify({ error: "RAILWAY_PROJECT_ID not set" }) }] };
186
- }
187
- // Get latest deployment
188
- const serviceFilter = serviceId ? `serviceId: "${serviceId}",` : "";
189
- const depRes = await fetch("https://backboard.railway.com/graphql/v2", {
190
- method: "POST",
191
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
192
- body: JSON.stringify({
193
- query: `{ deployments(first: 1, input: { projectId: "${projectId}", ${serviceFilter} }) { edges { node { id } } } }`,
194
- }),
195
- });
196
- const depData = (await depRes.json());
197
- const deploymentId = depData.data?.deployments?.edges?.[0]?.node?.id;
198
- if (!deploymentId) {
199
- return { content: [{ type: "text", text: JSON.stringify({ error: "No deployments found" }) }] };
200
- }
201
- const logRes = await fetch("https://backboard.railway.com/graphql/v2", {
202
- method: "POST",
203
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
204
- body: JSON.stringify({
205
- query: `{ deploymentLogs(deploymentId: "${deploymentId}", limit: ${lines}) { timestamp message severity } }`,
206
- }),
207
- });
208
- const logData = (await logRes.json());
209
- const logs = logData.data?.deploymentLogs ?? [];
210
- return {
211
- content: [{ type: "text", text: JSON.stringify({ provider: "railway", deploymentId, logs }, null, 2) }],
212
- };
213
- }
214
- // Vercel logs
215
- if ((provider === "vercel" || !provider) && process.env.VERCEL_TOKEN) {
216
- const token = process.env.VERCEL_TOKEN;
217
- const depRes = await fetch("https://api.vercel.com/v6/deployments?limit=1", {
218
- headers: { Authorization: `Bearer ${token}` },
219
- });
220
- const depData = (await depRes.json());
221
- const dep = depData.deployments?.[0];
222
- if (!dep) {
223
- return { content: [{ type: "text", text: JSON.stringify({ error: "No deployments found" }) }] };
224
- }
225
- const logRes = await fetch(`https://api.vercel.com/v2/deployments/${dep.uid}/events`, {
226
- headers: { Authorization: `Bearer ${token}` },
227
- });
228
- const events = (await logRes.json());
229
- const logs = Array.isArray(events)
230
- ? events
231
- .filter((e) => e.type === "stdout" || e.type === "stderr")
232
- .slice(-lines)
233
- .map((e) => ({
234
- timestamp: new Date(e.created).toISOString(),
235
- message: e.payload?.text ?? e.text ?? "",
236
- severity: e.type === "stderr" ? "ERROR" : "INFO",
237
- }))
238
- : [];
239
- return {
240
- content: [{ type: "text", text: JSON.stringify({ provider: "vercel", deploymentUrl: dep.url, logs }, null, 2) }],
241
- };
242
- }
243
- return {
244
- content: [{ type: "text", text: JSON.stringify({ error: "No log provider available. Set RAILWAY_API_TOKEN or VERCEL_TOKEN." }) }],
245
- };
246
- });
247
- // ──────────────────────────────────────────
248
- // Tool: stk_todo_list
249
- // ──────────────────────────────────────────
250
- server.tool("stk_todo_list", "List open GitHub issues for this project. Helps understand what needs to be worked on.", {
251
- label: z.string().optional().describe("Filter by label"),
252
- limit: z.number().optional().default(15).describe("Max issues to return"),
253
- }, async ({ label, limit }) => {
254
- const config = loadConfig();
255
- const repo = config.github?.repo ?? process.env.GITHUB_REPO ?? detectGitHubRepo();
256
- const token = process.env.GITHUB_TOKEN;
257
- if (!repo) {
258
- return { content: [{ type: "text", text: JSON.stringify({ error: "Could not detect GitHub repo. Set GITHUB_REPO or add github.repo to stk.config.json" }) }] };
259
- }
260
- const params = new URLSearchParams({
261
- state: "open",
262
- per_page: String(limit),
263
- sort: "updated",
264
- direction: "desc",
265
- });
266
- if (label)
267
- params.set("labels", label);
268
- const headers = { Accept: "application/vnd.github+json" };
269
- if (token)
270
- headers.Authorization = `Bearer ${token}`;
271
- const res = await fetch(`https://api.github.com/repos/${repo}/issues?${params}`, { headers });
272
- if (!res.ok) {
273
- return { content: [{ type: "text", text: JSON.stringify({ error: `GitHub API: ${res.status}` }) }] };
274
- }
275
- const issues = (await res.json());
276
- const filtered = issues
277
- .filter((i) => !i.pull_request)
278
- .map((i) => ({
279
- number: i.number,
280
- title: i.title,
281
- labels: i.labels.map((l) => l.name),
282
- assignee: i.assignee?.login ?? null,
283
- created: i.created_at,
284
- url: i.html_url,
285
- }));
286
- return {
287
- content: [{ type: "text", text: JSON.stringify({ repo, issues: filtered }, null, 2) }],
288
- };
289
- });
290
- // ──────────────────────────────────────────
291
- // Tool: stk_todo_add
292
- // ──────────────────────────────────────────
293
- server.tool("stk_todo_add", "Create a new GitHub issue for this project.", {
294
- title: z.string().describe("Issue title"),
295
- body: z.string().optional().describe("Issue body/description"),
296
- labels: z.array(z.string()).optional().describe("Labels to add"),
297
- }, async ({ title, body, labels }) => {
298
- const config = loadConfig();
299
- const repo = config.github?.repo ?? process.env.GITHUB_REPO ?? detectGitHubRepo();
300
- const token = process.env.GITHUB_TOKEN;
301
- if (!repo || !token) {
302
- return { content: [{ type: "text", text: JSON.stringify({ error: "Need GITHUB_TOKEN and repo to create issues" }) }] };
303
- }
304
- const payload = { title };
305
- if (body)
306
- payload.body = body;
307
- if (labels)
308
- payload.labels = labels;
309
- const res = await fetch(`https://api.github.com/repos/${repo}/issues`, {
310
- method: "POST",
311
- headers: {
312
- Authorization: `Bearer ${token}`,
313
- Accept: "application/vnd.github+json",
314
- "Content-Type": "application/json",
315
- },
316
- body: JSON.stringify(payload),
317
- });
318
- if (!res.ok) {
319
- const data = (await res.json());
320
- return { content: [{ type: "text", text: JSON.stringify({ error: data.message ?? `HTTP ${res.status}` }) }] };
321
- }
322
- const issue = (await res.json());
323
- return {
324
- content: [{ type: "text", text: JSON.stringify({ created: true, number: issue.number, url: issue.html_url }, null, 2) }],
325
- };
326
- });
327
- // ──────────────────────────────────────────
328
- // Tool: stk_deploy
329
- // ──────────────────────────────────────────
330
- server.tool("stk_deploy", "Push current branch to remote and trigger deploys. Use with caution — this pushes code to production.", {
331
- skipPush: z.boolean().optional().describe("Skip git push, just report current deploy status"),
332
- }, async ({ skipPush }) => {
333
- const config = loadConfig();
334
- const branch = config.deploy?.branch ?? "main";
335
- if (!skipPush) {
336
- try {
337
- const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
338
- if (currentBranch !== branch) {
339
- return {
340
- content: [{ type: "text", text: JSON.stringify({ error: `On branch "${currentBranch}", not "${branch}". Switch branches first.` }) }],
341
- };
342
- }
343
- execSync(`git push origin ${branch}`, { encoding: "utf-8", stdio: "pipe" });
344
- }
345
- catch (err) {
346
- return {
347
- content: [{ type: "text", text: JSON.stringify({ error: `Git push failed: ${err.message}` }) }],
348
- };
349
- }
350
- }
351
- return {
352
- content: [{
353
- type: "text",
354
- text: JSON.stringify({
355
- pushed: !skipPush,
356
- branch,
357
- providers: config.deploy?.providers ?? [],
358
- note: "Deploy triggered. Use stk_health to verify after a few minutes.",
359
- }, null, 2),
360
- }],
361
- };
362
- });
363
- // ──────────────────────────────────────────
364
- // Tool: stk_config
365
- // ──────────────────────────────────────────
366
- server.tool("stk_config", "Read the current stk configuration for this project. Shows which services are enabled, deploy settings, and project name.", {}, async () => {
367
- const config = loadConfig();
368
- return {
369
- content: [{ type: "text", text: JSON.stringify(config, null, 2) }],
370
- };
371
- });
372
- // ──────────────────────────────────────────
373
- // Tool: stk_db
374
- // ──────────────────────────────────────────
375
- server.tool("stk_db", "Query your Supabase database directly. Run SELECT queries, check row counts, inspect table data — all from chat. Only read operations are allowed for safety.", {
376
- query: z.string().optional().describe("SQL query to run (SELECT only for safety)"),
377
- table: z.string().optional().describe("Shorthand: just provide a table name to SELECT * with a limit"),
378
- limit: z.number().optional().describe("Max rows to return (default 20)"),
379
- }, async ({ query, table, limit: rawLimit }) => {
380
- const limit = rawLimit ?? 20;
381
- const url = process.env.SUPABASE_URL;
382
- const key = process.env.SUPABASE_SERVICE_KEY;
383
- if (!url || !key) {
384
- return { content: [{ type: "text", text: JSON.stringify({ error: "SUPABASE_URL or SUPABASE_SERVICE_KEY not set" }) }] };
385
- }
386
- // If table shorthand is used, build a simple query
387
- let sql = query ?? "";
388
- if (table && !query) {
389
- sql = `SELECT * FROM ${table} ORDER BY created_at DESC LIMIT ${limit}`;
390
- }
391
- if (!sql && !table) {
392
- return { content: [{ type: "text", text: JSON.stringify({ error: "Provide either a 'query' or 'table' parameter" }) }] };
393
- }
394
- // Safety: only allow read operations
395
- const normalized = sql.trim().toUpperCase();
396
- if (!normalized.startsWith("SELECT") && !normalized.startsWith("WITH")) {
397
- return { content: [{ type: "text", text: JSON.stringify({ error: "Only SELECT/WITH queries are allowed for safety. Use Supabase dashboard for mutations." }) }] };
398
- }
399
- try {
400
- const res = await fetch(`${url}/rest/v1/rpc/`, {
401
- method: "POST",
402
- headers: {
403
- apikey: key,
404
- Authorization: `Bearer ${key}`,
405
- "Content-Type": "application/json",
406
- Prefer: "return=representation",
407
- },
408
- body: JSON.stringify({}),
409
- });
410
- // Use PostgREST query instead of RPC for better compatibility
411
- // Parse table name from SQL for simple queries
412
- const tableMatch = sql.match(/FROM\s+["']?(\w+)["']?/i);
413
- const targetTable = table ?? tableMatch?.[1];
414
- if (targetTable) {
415
- const restRes = await fetch(`${url}/rest/v1/${targetTable}?select=*&limit=${limit}`, {
416
- headers: {
417
- apikey: key,
418
- Authorization: `Bearer ${key}`,
419
- "Content-Type": "application/json",
420
- Prefer: "count=exact",
421
- },
422
- });
423
- if (!restRes.ok) {
424
- const errText = await restRes.text();
425
- return { content: [{ type: "text", text: JSON.stringify({ error: `Query failed: ${errText}` }) }] };
426
- }
427
- const contentRange = restRes.headers.get("content-range");
428
- const totalCount = contentRange ? contentRange.split("/")[1] : "unknown";
429
- const data = await restRes.json();
430
- return {
431
- content: [{
432
- type: "text",
433
- text: JSON.stringify({
434
- table: targetTable,
435
- totalRows: totalCount,
436
- returned: Array.isArray(data) ? data.length : 0,
437
- data,
438
- }, null, 2),
439
- }],
440
- };
441
- }
442
- return { content: [{ type: "text", text: JSON.stringify({ error: "Could not parse table name from query. Use the 'table' parameter instead." }) }] };
443
- }
444
- catch (err) {
445
- return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
446
- }
447
- });
448
- // ──────────────────────────────────────────
449
- // Tool: stk_analytics
450
- // ──────────────────────────────────────────
451
- server.tool("stk_analytics", "Get live app analytics: total users, posts, payments, revenue, and recent activity. Pulls data from Supabase and Stripe in one call.", {}, async () => {
452
- const results = {};
453
- // Supabase stats
454
- const url = process.env.SUPABASE_URL;
455
- const key = process.env.SUPABASE_SERVICE_KEY;
456
- if (url && key) {
457
- const headers = {
458
- apikey: key,
459
- Authorization: `Bearer ${key}`,
460
- Prefer: "count=exact",
461
- };
462
- // Get table counts in parallel
463
- const [postsRes, usersRes, paymentsRes] = await Promise.all([
464
- fetch(`${url}/rest/v1/posts?select=id&limit=0`, { headers }).catch(() => null),
465
- fetch(`${url}/rest/v1/users?select=id&limit=0`, { headers }).catch(() => null),
466
- fetch(`${url}/rest/v1/payments?select=id&limit=0`, { headers }).catch(() => null),
467
- ]);
468
- const getCount = (res) => {
469
- if (!res?.ok)
470
- return null;
471
- const range = res.headers.get("content-range");
472
- return range ? parseInt(range.split("/")[1]) || 0 : null;
473
- };
474
- results.supabase = {
475
- totalPosts: getCount(postsRes),
476
- totalUsers: getCount(usersRes),
477
- totalPayments: getCount(paymentsRes),
478
- };
479
- // Recent posts (last 24h)
480
- const oneDayAgo = Math.floor(Date.now() / 1000) - 86400;
481
- const recentRes = await fetch(`${url}/rest/v1/posts?select=id&timestamp=gte.${oneDayAgo}&limit=0`, { headers: { ...headers, Prefer: "count=exact" } }).catch(() => null);
482
- results.supabase.postsLast24h = getCount(recentRes);
483
- // Recent users with paid posts
484
- const paidUsersRes = await fetch(`${url}/rest/v1/users?select=id&paid_posts_available=gt.0&limit=0`, { headers: { ...headers, Prefer: "count=exact" } }).catch(() => null);
485
- results.supabase.usersWithPaidPosts = getCount(paidUsersRes);
486
- }
487
- // Stripe stats
488
- const stripeKey = process.env.STRIPE_SECRET_KEY;
489
- if (stripeKey) {
490
- try {
491
- // Balance
492
- const balRes = await fetch("https://api.stripe.com/v1/balance", {
493
- headers: { Authorization: `Bearer ${stripeKey}` },
494
- });
495
- const balData = await balRes.json();
496
- // Recent charges
497
- const chargesRes = await fetch("https://api.stripe.com/v1/charges?limit=100", {
498
- headers: { Authorization: `Bearer ${stripeKey}` },
499
- });
500
- const chargesData = await chargesRes.json();
501
- const charges = chargesData.data ?? [];
502
- const totalRevenue = charges.reduce((sum, c) => sum + (c.status === "succeeded" ? c.amount : 0), 0);
503
- results.stripe = {
504
- balance: balData.available?.map((b) => `${(b.amount / 100).toFixed(2)} ${b.currency.toUpperCase()}`) ?? [],
505
- totalCharges: charges.length,
506
- successfulCharges: charges.filter((c) => c.status === "succeeded").length,
507
- totalRevenue: `${(totalRevenue / 100).toFixed(2)}`,
508
- mode: stripeKey.startsWith("sk_live") ? "live" : "test",
509
- };
510
- }
511
- catch (err) {
512
- results.stripe = { error: err.message };
513
- }
514
- }
515
- return {
516
- content: [{ type: "text", text: JSON.stringify({ analytics: results }, null, 2) }],
517
- };
518
- });
519
- // ──────────────────────────────────────────
520
- // Tool: stk_alerts
521
- // ──────────────────────────────────────────
522
- 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 () => {
523
- await loadPluginCheckers();
524
- const config = loadConfig();
525
- const alerts = [];
526
- // 1. Check all service health
527
- const serviceList = enabledServices(config);
528
- const checks = serviceList.map(async (name) => {
529
- const checker = getChecker(name);
530
- if (!checker)
531
- return null;
532
- return checker();
533
- });
534
- const results = (await Promise.all(checks)).filter(Boolean);
535
- for (const r of results) {
536
- if (r.status === "down") {
537
- alerts.push({ level: "critical", source: r.name, message: `Service is DOWN: ${r.detail ?? "unreachable"}` });
538
- }
539
- else if (r.status === "degraded") {
540
- alerts.push({ level: "warning", source: r.name, message: `Service degraded: ${r.detail ?? "slow response"}` });
541
- }
542
- else if (r.latency && r.latency > 3000) {
543
- alerts.push({ level: "warning", source: r.name, message: `High latency: ${r.latency}ms` });
544
- }
545
- }
546
- // 2. Check Vercel for failed deploys
547
- if (process.env.VERCEL_TOKEN) {
548
- try {
549
- const res = await fetch("https://api.vercel.com/v6/deployments?limit=5", {
550
- headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
551
- });
552
- const data = await res.json();
553
- for (const dep of data.deployments ?? []) {
554
- const state = dep.readyState ?? dep.state;
555
- if (state === "ERROR" || state === "CANCELED") {
556
- alerts.push({ level: "critical", source: "Vercel", message: `Deploy ${state}: ${dep.url ?? dep.uid}` });
557
- }
558
- }
559
- }
560
- catch { /* skip */ }
561
- }
562
- // 3. Check Stripe for recent failures
563
- if (process.env.STRIPE_SECRET_KEY) {
564
- try {
565
- const res = await fetch("https://api.stripe.com/v1/charges?limit=20", {
566
- headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}` },
567
- });
568
- const data = await res.json();
569
- const failed = (data.data ?? []).filter((c) => c.status === "failed");
570
- if (failed.length > 0) {
571
- alerts.push({ level: "warning", source: "Stripe", message: `${failed.length} failed charge(s) in recent transactions` });
572
- }
573
- }
574
- catch { /* skip */ }
575
- }
576
- // 4. Check for error logs in Vercel
577
- if (process.env.VERCEL_TOKEN) {
578
- try {
579
- const depRes = await fetch("https://api.vercel.com/v6/deployments?limit=1", {
580
- headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
581
- });
582
- const depData = await depRes.json();
583
- const dep = depData.deployments?.[0];
584
- if (dep) {
585
- const logRes = await fetch(`https://api.vercel.com/v2/deployments/${dep.uid}/events`, {
586
- headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
587
- });
588
- const events = await logRes.json();
589
- if (Array.isArray(events)) {
590
- const errors = events.filter((e) => e.type === "stderr");
591
- if (errors.length > 5) {
592
- alerts.push({ level: "warning", source: "Vercel Logs", message: `${errors.length} stderr entries in latest deploy` });
593
- }
594
- }
595
- }
596
- }
597
- catch { /* skip */ }
598
- }
599
- if (alerts.length === 0) {
600
- alerts.push({ level: "info", source: "stk", message: "All clear — no issues detected" });
601
- }
602
- return {
603
- content: [{
604
- type: "text",
605
- text: JSON.stringify({
606
- project: config.name,
607
- alerts,
608
- summary: {
609
- critical: alerts.filter((a) => a.level === "critical").length,
610
- warnings: alerts.filter((a) => a.level === "warning").length,
611
- ok: alerts.every((a) => a.level === "info"),
612
- },
613
- }, null, 2),
614
- }],
615
- };
616
- });
617
- // ──────────────────────────────────────────
618
- // Tool: stk_rollback
619
- // ──────────────────────────────────────────
620
- server.tool("stk_rollback", "Rollback to a previous Vercel deployment. Lists recent deploys and can promote an older one to production.", {
621
- deployId: z.string().optional().describe("Deployment ID to rollback to. If omitted, lists recent deployments to choose from."),
622
- confirm: z.boolean().optional().default(false).describe("Must be true to actually execute the rollback"),
623
- }, async ({ deployId, confirm }) => {
624
- const token = process.env.VERCEL_TOKEN;
625
- if (!token) {
626
- return { content: [{ type: "text", text: JSON.stringify({ error: "VERCEL_TOKEN not set" }) }] };
627
- }
628
- // List recent deployments
629
- const depRes = await fetch("https://api.vercel.com/v6/deployments?limit=10", {
630
- headers: { Authorization: `Bearer ${token}` },
631
- });
632
- const depData = await depRes.json();
633
- const deployments = (depData.deployments ?? []).map((d) => ({
634
- id: d.uid,
635
- url: d.url,
636
- state: d.readyState ?? d.state,
637
- created: new Date(d.created).toISOString(),
638
- target: d.target ?? "preview",
639
- }));
640
- if (!deployId) {
641
- return {
642
- content: [{
643
- type: "text",
644
- text: JSON.stringify({
645
- message: "Recent deployments — provide a deployId to rollback",
646
- deployments,
647
- }, null, 2),
648
- }],
649
- };
650
- }
651
- if (!confirm) {
652
- const target = deployments.find((d) => d.id === deployId);
653
- return {
654
- content: [{
655
- type: "text",
656
- text: JSON.stringify({
657
- message: "Rollback requires confirmation. Call again with confirm: true",
658
- target: target ?? deployId,
659
- }, null, 2),
660
- }],
661
- };
662
- }
663
- // Execute rollback by promoting the old deployment
664
- try {
665
- // Get the deployment's project
666
- const detailRes = await fetch(`https://api.vercel.com/v13/deployments/${deployId}`, {
667
- headers: { Authorization: `Bearer ${token}` },
668
- });
669
- const detail = await detailRes.json();
670
- const projectId = detail.projectId;
671
- if (!projectId) {
672
- return { content: [{ type: "text", text: JSON.stringify({ error: "Could not determine project from deployment" }) }] };
673
- }
674
- // Create a new deployment based on the old one (redeploy)
675
- const rollbackRes = await fetch(`https://api.vercel.com/v13/deployments`, {
676
- method: "POST",
677
- headers: {
678
- Authorization: `Bearer ${token}`,
679
- "Content-Type": "application/json",
680
- },
681
- body: JSON.stringify({
682
- name: detail.name,
683
- deploymentId: deployId,
684
- target: "production",
685
- }),
686
- });
687
- if (!rollbackRes.ok) {
688
- const errData = await rollbackRes.json();
689
- return { content: [{ type: "text", text: JSON.stringify({ error: errData.error?.message ?? `HTTP ${rollbackRes.status}` }) }] };
690
- }
691
- const rollbackData = await rollbackRes.json();
692
- return {
693
- content: [{
694
- type: "text",
695
- text: JSON.stringify({
696
- rolledBack: true,
697
- newDeploymentId: rollbackData.id,
698
- url: rollbackData.url,
699
- note: "Rollback triggered. Use stk_health to verify.",
700
- }, null, 2),
701
- }],
702
- };
703
- }
704
- catch (err) {
705
- return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
706
- }
707
- });
708
- // ──────────────────────────────────────────
709
- // Tool: stk_env_sync
710
- // ──────────────────────────────────────────
711
- server.tool("stk_env_sync", "Compare and sync environment variables between local .env and Vercel. Shows which vars are missing, extra, or mismatched.", {
712
- action: z.enum(["diff", "pull", "push"]).optional().default("diff").describe("diff: compare local vs remote. pull: download remote to .env.pulled. push: upload local to Vercel."),
713
- confirm: z.boolean().optional().default(false).describe("Required for push action"),
714
- }, async ({ action, confirm }) => {
715
- const token = process.env.VERCEL_TOKEN;
716
- const projectId = process.env.VERCEL_PROJECT_ID;
717
- // Read local .env
718
- let localVars = {};
719
- try {
720
- const { readFileSync } = await import("fs");
721
- const envContent = readFileSync(".env", "utf-8");
722
- for (const line of envContent.split("\n")) {
723
- const trimmed = line.trim();
724
- if (!trimmed || trimmed.startsWith("#"))
725
- continue;
726
- const eqIdx = trimmed.indexOf("=");
727
- if (eqIdx > 0) {
728
- localVars[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
729
- }
730
- }
731
- }
732
- catch {
733
- localVars = {};
734
- }
735
- if (!token) {
736
- return {
737
- content: [{
738
- type: "text",
739
- text: JSON.stringify({
740
- localVars: Object.keys(localVars),
741
- remote: "VERCEL_TOKEN not set — cannot fetch remote env vars",
742
- }, null, 2),
743
- }],
744
- };
745
- }
746
- // Fetch Vercel env vars
747
- let remoteVars = {};
748
- const envUrl = projectId
749
- ? `https://api.vercel.com/v9/projects/${projectId}/env`
750
- : null;
751
- if (envUrl) {
752
- try {
753
- const res = await fetch(envUrl, {
754
- headers: { Authorization: `Bearer ${token}` },
755
- });
756
- const data = await res.json();
757
- for (const env of data.envs ?? []) {
758
- remoteVars[env.key] = env.value ?? "(encrypted)";
759
- }
760
- }
761
- catch { /* skip */ }
762
- }
763
- const localKeys = new Set(Object.keys(localVars));
764
- const remoteKeys = new Set(Object.keys(remoteVars));
765
- const onlyLocal = [...localKeys].filter((k) => !remoteKeys.has(k));
766
- const onlyRemote = [...remoteKeys].filter((k) => !localKeys.has(k));
767
- const shared = [...localKeys].filter((k) => remoteKeys.has(k));
768
- if (action === "diff" || !action) {
769
- return {
770
- content: [{
771
- type: "text",
772
- text: JSON.stringify({
773
- localCount: localKeys.size,
774
- remoteCount: remoteKeys.size,
775
- onlyInLocal: onlyLocal,
776
- onlyInRemote: onlyRemote,
777
- inBoth: shared.length,
778
- note: projectId ? undefined : "Set VERCEL_PROJECT_ID for remote env comparison",
779
- }, null, 2),
780
- }],
781
- };
782
- }
783
- if (action === "pull") {
784
- const { writeFileSync } = await import("fs");
785
- const lines = Object.entries(remoteVars).map(([k, v]) => `${k}=${v}`);
786
- writeFileSync(".env.pulled", lines.join("\n") + "\n");
787
- return {
788
- content: [{ type: "text", text: JSON.stringify({ pulled: true, file: ".env.pulled", count: lines.length }) }],
789
- };
790
- }
791
- if (action === "push") {
792
- if (!confirm) {
793
- return {
794
- content: [{ type: "text", text: JSON.stringify({ message: "Push requires confirm: true. This will overwrite remote env vars.", varsToUpload: onlyLocal.length + shared.length }) }],
795
- };
796
- }
797
- if (!projectId || !envUrl) {
798
- return { content: [{ type: "text", text: JSON.stringify({ error: "VERCEL_PROJECT_ID required for push" }) }] };
799
- }
800
- let uploaded = 0;
801
- for (const [key, value] of Object.entries(localVars)) {
802
- await fetch(envUrl, {
803
- method: "POST",
804
- headers: {
805
- Authorization: `Bearer ${token}`,
806
- "Content-Type": "application/json",
807
- },
808
- body: JSON.stringify({
809
- key,
810
- value,
811
- type: "encrypted",
812
- target: ["production", "preview", "development"],
813
- }),
814
- });
815
- uploaded++;
816
- }
817
- return {
818
- content: [{ type: "text", text: JSON.stringify({ pushed: true, uploaded }) }],
819
- };
820
- }
821
- return { content: [{ type: "text", text: JSON.stringify({ error: "Unknown action" }) }] };
822
- });
823
- // ──────────────────────────────────────────
824
- // Tool: stk_perf
825
- // ──────────────────────────────────────────
826
- server.tool("stk_perf", "Check performance across your stack: Supabase query latency, table sizes, Vercel deploy times, and API response times.", {
827
- tables: z.array(z.string()).optional().describe("Specific Supabase tables to benchmark (defaults to all detected)"),
828
- }, async ({ tables }) => {
829
- const perf = {};
830
- // Supabase performance
831
- const url = process.env.SUPABASE_URL;
832
- const key = process.env.SUPABASE_SERVICE_KEY;
833
- if (url && key) {
834
- const headers = {
835
- apikey: key,
836
- Authorization: `Bearer ${key}`,
837
- Prefer: "count=exact",
838
- };
839
- // Auto-detect tables or use provided list
840
- const tablesToCheck = tables ?? ["posts", "users", "payments"];
841
- const tableStats = [];
842
- for (const table of tablesToCheck) {
843
- const start = Date.now();
844
- try {
845
- const res = await fetch(`${url}/rest/v1/${table}?select=id&limit=0`, {
846
- headers,
847
- });
848
- const latency = Date.now() - start;
849
- const range = res.headers.get("content-range");
850
- const count = range ? parseInt(range.split("/")[1]) || 0 : null;
851
- let status = "fast";
852
- if (latency > 1000)
853
- status = "slow";
854
- else if (latency > 500)
855
- status = "moderate";
856
- tableStats.push({ table, rowCount: count, queryLatency: latency, status });
857
- }
858
- catch {
859
- tableStats.push({ table, rowCount: null, queryLatency: Date.now() - start, status: "error" });
860
- }
861
- }
862
- // API latency test
863
- const apiStart = Date.now();
864
- await fetch(`${url}/rest/v1/`, { headers }).catch(() => null);
865
- const apiLatency = Date.now() - apiStart;
866
- perf.supabase = {
867
- apiLatency,
868
- tables: tableStats,
869
- recommendation: tableStats.some((t) => t.status === "slow")
870
- ? "Some queries are slow. Consider adding database indexes."
871
- : tableStats.some((t) => (t.rowCount ?? 0) > 10000)
872
- ? "Large tables detected. Ensure you have indexes on frequently queried columns."
873
- : "Performance looks good.",
874
- };
875
- }
876
- // Vercel deploy performance
877
- if (process.env.VERCEL_TOKEN) {
878
- try {
879
- const res = await fetch("https://api.vercel.com/v6/deployments?limit=5", {
880
- headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
881
- });
882
- const data = await res.json();
883
- const deploys = (data.deployments ?? []).map((d) => {
884
- const buildDuration = d.buildingAt && d.ready
885
- ? Math.round((d.ready - d.buildingAt) / 1000)
886
- : null;
887
- return {
888
- id: d.uid,
889
- state: d.readyState ?? d.state,
890
- buildDuration: buildDuration ? `${buildDuration}s` : "unknown",
891
- created: new Date(d.created).toISOString(),
892
- };
893
- });
894
- perf.vercel = {
895
- recentDeploys: deploys,
896
- avgBuildTime: deploys.filter((d) => d.buildDuration !== "unknown").length > 0
897
- ? "see individual deploys"
898
- : "no build data available",
899
- };
900
- }
901
- catch { /* skip */ }
902
- }
903
- // Stripe API latency
904
- if (process.env.STRIPE_SECRET_KEY) {
905
- const start = Date.now();
906
- await fetch("https://api.stripe.com/v1/balance", {
907
- headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}` },
908
- }).catch(() => null);
909
- perf.stripe = { apiLatency: Date.now() - start };
910
- }
911
- return {
912
- content: [{ type: "text", text: JSON.stringify({ performance: perf }, null, 2) }],
913
- };
914
- });
915
- // ──────────────────────────────────────────
916
- // Tool: stk_cost
917
- // ──────────────────────────────────────────
918
- server.tool("stk_cost", "Track costs across your stack: Stripe fees, Vercel usage, Supabase plan details. Get a unified view of what you're spending.", {}, async () => {
919
- const costs = {};
920
- // Stripe revenue & fees
921
- if (process.env.STRIPE_SECRET_KEY) {
922
- try {
923
- const stripeKey = process.env.STRIPE_SECRET_KEY;
924
- // Balance
925
- const balRes = await fetch("https://api.stripe.com/v1/balance", {
926
- headers: { Authorization: `Bearer ${stripeKey}` },
927
- });
928
- const balData = await balRes.json();
929
- // Recent balance transactions for fee tracking
930
- const txRes = await fetch("https://api.stripe.com/v1/balance_transactions?limit=100", {
931
- headers: { Authorization: `Bearer ${stripeKey}` },
932
- });
933
- const txData = await txRes.json();
934
- const transactions = txData.data ?? [];
935
- const totalFees = transactions.reduce((sum, t) => sum + (t.fee || 0), 0);
936
- const totalGross = transactions.reduce((sum, t) => sum + (t.amount > 0 ? t.amount : 0), 0);
937
- const totalNet = transactions.reduce((sum, t) => sum + (t.net || 0), 0);
938
- costs.stripe = {
939
- mode: stripeKey.startsWith("sk_live") ? "live" : "test",
940
- balance: balData.available?.map((b) => ({
941
- amount: (b.amount / 100).toFixed(2),
942
- currency: b.currency.toUpperCase(),
943
- })) ?? [],
944
- recentTransactions: transactions.length,
945
- totalGross: (totalGross / 100).toFixed(2),
946
- totalFees: (totalFees / 100).toFixed(2),
947
- totalNet: (totalNet / 100).toFixed(2),
948
- feePercentage: totalGross > 0 ? ((totalFees / totalGross) * 100).toFixed(1) + "%" : "N/A",
949
- };
950
- }
951
- catch (err) {
952
- costs.stripe = { error: err.message };
953
- }
954
- }
955
- // Vercel usage
956
- if (process.env.VERCEL_TOKEN) {
957
- try {
958
- // Get team/user info for billing context
959
- const userRes = await fetch("https://api.vercel.com/v2/user", {
960
- headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
961
- });
962
- const userData = await userRes.json();
963
- // Count deployments
964
- const depRes = await fetch("https://api.vercel.com/v6/deployments?limit=100", {
965
- headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
966
- });
967
- const depData = await depRes.json();
968
- const deployments = depData.deployments ?? [];
969
- // Deployments this month
970
- const monthStart = new Date();
971
- monthStart.setDate(1);
972
- monthStart.setHours(0, 0, 0, 0);
973
- const thisMonth = deployments.filter((d) => new Date(d.created) >= monthStart);
974
- costs.vercel = {
975
- plan: userData.user?.billing?.plan ?? "unknown",
976
- deploymentsThisMonth: thisMonth.length,
977
- totalDeployments: deployments.length,
978
- note: "Vercel free tier includes 100 deployments/day. Check vercel.com/dashboard for detailed billing.",
979
- };
980
- }
981
- catch { /* skip */ }
982
- }
983
- // Supabase usage estimate
984
- if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_KEY) {
985
- const url = process.env.SUPABASE_URL;
986
- const key = process.env.SUPABASE_SERVICE_KEY;
987
- const headers = {
988
- apikey: key,
989
- Authorization: `Bearer ${key}`,
990
- Prefer: "count=exact",
991
- };
992
- const tableCounts = {};
993
- for (const table of ["posts", "users", "payments"]) {
994
- try {
995
- const res = await fetch(`${url}/rest/v1/${table}?select=id&limit=0`, { headers });
996
- const range = res.headers.get("content-range");
997
- tableCounts[table] = range ? parseInt(range.split("/")[1]) || 0 : null;
998
- }
999
- catch {
1000
- tableCounts[table] = null;
1001
- }
1002
- }
1003
- const totalRows = Object.values(tableCounts).reduce((s, v) => s + (v ?? 0), 0);
1004
- costs.supabase = {
1005
- tables: tableCounts,
1006
- totalRows,
1007
- note: "Supabase free tier: 500MB database, 1GB storage, 50k monthly active users. Check supabase.com/dashboard for detailed usage.",
1008
- };
1009
- }
1010
- return {
1011
- content: [{ type: "text", text: JSON.stringify({ costs }, null, 2) }],
1012
- };
1013
- });
1014
- // ──────────────────────────────────────────
1015
- // Brain: Supabase Knowledge Base Client
1016
- // ──────────────────────────────────────────
1017
- function getBrainClient() {
1018
- return getLocalBrainClient();
1019
- }
1020
- // ──────────────────────────────────────────
1021
- // Tool: stk_brain_search
1022
- // ──────────────────────────────────────────
1023
- server.tool("stk_brain_search", "Search the knowledge base for SaaS patterns, best practices, and architecture examples from top open-source projects (LangChain, Ollama, Transformers, llama.cpp, vLLM, AutoGen, OpenAI Cookbook). Use this when you need to know how successful projects solve specific problems.", {
1024
- query: z.string().describe("What to search for (e.g., 'authentication', 'real-time updates', 'payment integration')"),
1025
- category: z.string().optional().describe("Filter: architecture, auth, payments, database, api, deployment, testing, performance, security, ml, realtime, general"),
1026
- }, async ({ query, category }) => {
1027
- const brain = getBrainClient();
1028
- // Try ilike search on content and title
1029
- const words = query.split(" ").filter(w => w.length > 2);
1030
- const searchWord = words[0] ?? query;
1031
- const params = {
1032
- or: `(title.ilike.%${searchWord}%,content.ilike.%${searchWord}%)`,
1033
- limit: "10",
1034
- order: "source",
1035
- };
1036
- if (category)
1037
- params["category"] = `eq.${category}`;
1038
- const { data, ok } = await brain.query("knowledge", params);
1039
- if (!ok)
1040
- return { content: [{ type: "text", text: JSON.stringify({ error: "Query failed", data }) }] };
1041
- return {
1042
- content: [{
1043
- type: "text",
1044
- text: JSON.stringify({
1045
- query,
1046
- results: (data ?? []).map((r) => ({ source: r.source, category: r.category, title: r.title, content: r.content, tags: r.tags })),
1047
- total: data?.length ?? 0,
1048
- }, null, 2),
1049
- }],
1050
- };
1051
- });
1052
- // ──────────────────────────────────────────
1053
- // Tool: stk_brain_patterns
1054
- // ──────────────────────────────────────────
1055
- server.tool("stk_brain_patterns", "Get best practice patterns for a specific feature. Returns how top projects implement auth, payments, real-time, caching, APIs, etc.", {
1056
- feature: z.string().describe("The feature or pattern (e.g., 'authentication', 'webhooks', 'caching', 'model serving', 'fine-tuning')"),
1057
- }, async ({ feature }) => {
1058
- const brain = getBrainClient();
1059
- const { data, ok } = await brain.query("knowledge", {
1060
- or: `(title.ilike.%${feature}%,content.ilike.%${feature}%)`,
1061
- limit: "15",
1062
- order: "source",
1063
- });
1064
- if (!ok)
1065
- return { content: [{ type: "text", text: JSON.stringify({ error: "Query failed" }) }] };
1066
- // Group by source
1067
- const grouped = {};
1068
- for (const item of data ?? []) {
1069
- if (!grouped[item.source])
1070
- grouped[item.source] = [];
1071
- grouped[item.source].push({ title: item.title, category: item.category, content: item.content });
1072
- }
1073
- return {
1074
- content: [{
1075
- type: "text",
1076
- text: JSON.stringify({
1077
- feature,
1078
- patterns: grouped,
1079
- totalSources: Object.keys(grouped).length,
1080
- totalPatterns: data?.length ?? 0,
1081
- }, null, 2),
1082
- }],
1083
- };
1084
- });
1085
- // ──────────────────────────────────────────
1086
- // Tool: stk_brain_stack
1087
- // ──────────────────────────────────────────
1088
- server.tool("stk_brain_stack", "Get recommendations specific to YOUR stack (Supabase + Vercel + Stripe + Node.js). Filters knowledge for patterns matching your technology choices.", {
1089
- question: z.string().describe("What you want to build or solve (e.g., 'add user auth', 'implement webhooks', 'optimize queries')"),
1090
- }, async ({ question }) => {
1091
- const brain = getBrainClient();
1092
- const words = question.split(" ").filter(w => w.length > 3);
1093
- const searchWord = words[0] ?? question;
1094
- const { data, ok } = await brain.query("knowledge", {
1095
- or: `(content.ilike.%${searchWord}%,title.ilike.%${searchWord}%)`,
1096
- limit: "10",
1097
- });
1098
- if (!ok)
1099
- return { content: [{ type: "text", text: JSON.stringify({ error: "Query failed" }) }] };
1100
- return {
1101
- content: [{
1102
- type: "text",
1103
- text: JSON.stringify({
1104
- question,
1105
- stack: ["Supabase", "Vercel", "Stripe", "Node.js/Express"],
1106
- recommendations: (data ?? []).map((r) => ({ source: r.source, title: r.title, content: r.content, relevance: r.category })),
1107
- total: data?.length ?? 0,
1108
- }, null, 2),
1109
- }],
1110
- };
1111
- });
1112
- // ──────────────────────────────────────────
1113
- // Tool: stk_brain_learn
1114
- // ──────────────────────────────────────────
1115
- server.tool("stk_brain_learn", "Save new knowledge to the brain. Use this to remember patterns, solutions, or learnings for future reference across all projects.", {
1116
- title: z.string().describe("Short title"),
1117
- content: z.string().describe("The knowledge — pattern, solution, or best practice"),
1118
- source: z.string().optional().describe("Where this came from (e.g., 'project:worldchat', 'github:vercel/next.js')"),
1119
- category: z.string().optional().describe("Category: architecture, auth, payments, database, api, deployment, testing, performance, security, ml, general"),
1120
- tags: z.array(z.string()).optional().describe("Tags for searchability"),
1121
- }, async ({ title, content, source, category, tags }) => {
1122
- const brain = getBrainClient();
1123
- const { data, ok } = await brain.insert("knowledge", {
1124
- title,
1125
- content,
1126
- source: source ?? "manual",
1127
- category: category ?? "general",
1128
- tags: tags ?? [],
1129
- created_at: new Date().toISOString(),
1130
- });
1131
- if (!ok)
1132
- return { content: [{ type: "text", text: JSON.stringify({ error: "Insert failed", data }) }] };
1133
- return {
1134
- content: [{
1135
- type: "text",
1136
- text: JSON.stringify({ learned: true, title, message: "Knowledge saved. I can recall this in future conversations." }, null, 2),
1137
- }],
1138
- };
1139
- });
1140
- // ──────────────────────────────────────────
1141
- // Tool: stk_brain_stats
1142
- // ──────────────────────────────────────────
1143
- server.tool("stk_brain_stats", "Check what the brain knows — total knowledge entries, categories, sources, and coverage.", {}, async () => {
1144
- const brain = getBrainClient();
1145
- const { data, count } = await brain.query("knowledge", { select: "category,source", limit: "1000" });
1146
- const categories = {};
1147
- const sources = {};
1148
- for (const row of data ?? []) {
1149
- categories[row.category] = (categories[row.category] || 0) + 1;
1150
- sources[row.source] = (sources[row.source] || 0) + 1;
1151
- }
1152
- return {
1153
- content: [{
1154
- type: "text",
1155
- text: JSON.stringify({
1156
- totalKnowledge: count ?? data?.length ?? 0,
1157
- categories,
1158
- sources,
1159
- topSources: Object.entries(sources)
1160
- .sort(([, a], [, b]) => b - a)
1161
- .slice(0, 10)
1162
- .map(([name, count]) => ({ name, count })),
1163
- }, null, 2),
1164
- }],
1165
- };
1166
- });
1167
- // ──────────────────────────────────────────
1168
- // Tool: stk_brain_check
1169
- // ──────────────────────────────────────────
1170
- server.tool("stk_brain_check", "PROACTIVE GOTCHA DETECTION — Run this BEFORE implementing any feature or fixing any bug. Searches the brain for known gotchas, pitfalls, and patterns related to your task. Returns warnings ranked by relevance so you can avoid repeating past mistakes. Claude should call this automatically before writing code.", {
1171
- task: z.string().describe("What you're about to implement or fix (e.g., 'add email verification', 'update user model', 'fix auth redirect')"),
1172
- }, async ({ task }) => {
1173
- const results = brainCheck(task);
1174
- if (results.length === 0) {
1175
- return {
1176
- content: [{
1177
- type: "text",
1178
- text: JSON.stringify({
1179
- task,
1180
- warnings: [],
1181
- message: "No known gotchas found. Proceed with caution — this might be new territory.",
1182
- }, null, 2),
1183
- }],
1184
- };
1185
- }
1186
- const warnings = results.slice(0, 5).map(r => ({
1187
- title: r.entry.title,
1188
- content: r.entry.content,
1189
- relevance: r.score,
1190
- matchedTerms: r.matchedTerms,
1191
- source: r.entry.source,
1192
- category: r.entry.category,
1193
- }));
1194
- return {
1195
- content: [{
1196
- type: "text",
1197
- text: JSON.stringify({
1198
- task,
1199
- warnings,
1200
- totalMatches: results.length,
1201
- message: `Found ${results.length} relevant pattern(s). Review warnings before coding.`,
1202
- }, null, 2),
1203
- }],
1204
- };
1205
- });
1206
- // ──────────────────────────────────────────
1207
- // Tool: stk_brain_diagnose
1208
- // ──────────────────────────────────────────
1209
- server.tool("stk_brain_diagnose", "ERROR PATTERN MATCHING — Run this when you encounter an error or bug. Searches the brain for matching patterns from past issues and returns known solutions. Claude should call this automatically when debugging.", {
1210
- error: z.string().describe("The error message, symptom, or bug description (e.g., 'redirect not working after verification', 'emails not sending', 'prisma field undefined')"),
1211
- }, async ({ error }) => {
1212
- const results = brainDiagnose(error);
1213
- if (results.length === 0) {
1214
- return {
1215
- content: [{
1216
- type: "text",
1217
- text: JSON.stringify({
1218
- error,
1219
- solutions: [],
1220
- message: "No matching patterns found. This is a new issue — after fixing it, use stk_brain_learn to save the pattern.",
1221
- }, null, 2),
1222
- }],
1223
- };
1224
- }
1225
- const solutions = results.slice(0, 5).map(r => ({
1226
- title: r.entry.title,
1227
- solution: r.entry.content,
1228
- relevance: r.score,
1229
- matchedTerms: r.matchedTerms,
1230
- source: r.entry.source,
1231
- }));
1232
- return {
1233
- content: [{
1234
- type: "text",
1235
- text: JSON.stringify({
1236
- error,
1237
- solutions,
1238
- totalMatches: results.length,
1239
- message: `Found ${results.length} matching pattern(s). Apply relevant solutions.`,
1240
- }, null, 2),
1241
- }],
1242
- };
1243
- });
1244
- // ──────────────────────────────────────────
1245
- // Tool: stk_brain_ingest
1246
- // ──────────────────────────────────────────
1247
- server.tool("stk_brain_ingest", "Scan the current project and ingest architecture knowledge into the local brain (~/.stk/brain.json). Automatically reads CLAUDE.md, package.json, Prisma schema, Dockerfile, CI config, and route files. Run this when setting up stk in a new project or after major changes.", {
1248
- force: z.boolean().optional().default(false).describe("Re-ingest even if already ingested"),
1249
- }, async ({ force }) => {
1250
- const store = loadBrainStore();
1251
- const { projectName, entries, filesScanned } = ingestProject(process.cwd());
1252
- if (store.projects[projectName] && !force) {
1253
- const existing = store.projects[projectName];
1254
- return {
1255
- content: [{
1256
- type: "text",
1257
- text: JSON.stringify({
1258
- alreadyIngested: true,
1259
- projectName,
1260
- entries: existing.entries.length,
1261
- ingestedAt: existing.ingestedAt,
1262
- message: "Already ingested. Use force: true to re-ingest.",
1263
- }, null, 2),
1264
- }],
1265
- };
1266
- }
1267
- if (entries.length === 0) {
1268
- return {
1269
- content: [{
1270
- type: "text",
1271
- text: JSON.stringify({ error: "No knowledge extracted. Make sure you're in a project directory with recognizable files." }),
1272
- }],
1273
- };
1274
- }
1275
- store.projects[projectName] = {
1276
- ingestedAt: new Date().toISOString(),
1277
- projectPath: process.cwd(),
1278
- entries,
1279
- };
1280
- saveBrainStore(store);
1281
- const categories = {};
1282
- for (const e of entries) {
1283
- categories[e.category] = (categories[e.category] || 0) + 1;
1284
- }
1285
- return {
1286
- content: [{
1287
- type: "text",
1288
- text: JSON.stringify({
1289
- ingested: true,
1290
- projectName,
1291
- totalEntries: entries.length,
1292
- filesScanned,
1293
- categories,
1294
- storedAt: "~/.stk/brain.json",
1295
- }, null, 2),
1296
- }],
1297
- };
1298
- });
1299
- // ──────────────────────────────────────────
1300
- // Tool: stk_brain_sync
1301
- // ──────────────────────────────────────────
1302
- server.tool("stk_brain_sync", "Sync brain knowledge between local (~/.stk/brain.json) and cloud (Supabase). Push shares your knowledge with other developers. Pull downloads knowledge from the cloud. Sync does both.", {
1303
- action: z.enum(["sync", "push", "pull"]).optional().default("sync").describe("sync: push+pull, push: local→cloud, pull: cloud→local"),
1304
- }, async ({ action }) => {
1305
- let result;
1306
- if (action === "push")
1307
- result = await pushToCloud();
1308
- else if (action === "pull")
1309
- result = await pullFromCloud();
1310
- else
1311
- result = await syncBrain();
1312
- return {
1313
- content: [{
1314
- type: "text",
1315
- text: JSON.stringify({
1316
- action,
1317
- pushed: result.pushed,
1318
- pulled: result.pulled,
1319
- errors: result.errors.length > 0 ? result.errors : undefined,
1320
- ok: result.errors.length === 0,
1321
- }, null, 2),
1322
- }],
1323
- };
1324
- });
1325
- // ──────────────────────────────────────────
1326
- // Tool: stk_brain_check (PROACTIVE — call before writing code)
1327
- // ──────────────────────────────────────────
1328
- server.tool("stk_brain_check", "PROACTIVE: Call this BEFORE implementing a feature or making changes. Searches the brain for known gotchas, past bugs, and patterns relevant to what you're about to build. Returns warnings that can prevent mistakes. Use this whenever you start a non-trivial coding task.", {
1329
- task: z.string().describe("What you're about to implement (e.g., 'add email verification', 'update user model', 'add webhook endpoint')"),
1330
- }, async ({ task }) => {
1331
- const results = brainCheck(task);
1332
- if (results.length === 0) {
1333
- return {
1334
- content: [{
1335
- type: "text",
1336
- text: JSON.stringify({
1337
- task,
1338
- warnings: [],
1339
- message: "No known gotchas found. Proceed carefully.",
1340
- }, null, 2),
1341
- }],
1342
- };
1343
- }
1344
- const warnings = results.slice(0, 8).map(r => ({
1345
- title: r.entry.title,
1346
- warning: r.entry.content,
1347
- relevance: r.score,
1348
- matchedTerms: r.matchedTerms,
1349
- source: r.entry.source,
1350
- category: r.entry.category,
1351
- }));
1352
- return {
1353
- content: [{
1354
- type: "text",
1355
- text: JSON.stringify({
1356
- task,
1357
- warnings,
1358
- totalMatches: results.length,
1359
- message: `Found ${results.length} relevant entries. Review warnings before coding.`,
1360
- }, null, 2),
1361
- }],
1362
- };
1363
- });
1364
- // ──────────────────────────────────────────
1365
- // Tool: stk_brain_diagnose (REACTIVE — call when you hit an error)
1366
- // ──────────────────────────────────────────
1367
- server.tool("stk_brain_diagnose", "REACTIVE: Call this when you encounter an error or bug. Searches the brain for matching patterns from past issues and returns known solutions. Use this before debugging from scratch — the answer may already be in the brain.", {
1368
- error: z.string().describe("The error message, bug description, or unexpected behavior you're seeing"),
1369
- }, async ({ error }) => {
1370
- const results = brainDiagnose(error);
1371
- if (results.length === 0) {
1372
- return {
1373
- content: [{
1374
- type: "text",
1375
- text: JSON.stringify({
1376
- error,
1377
- solutions: [],
1378
- message: "No matching patterns found in the brain. This is a new issue — debug it, fix it, then use stk_brain_learn to save the solution.",
1379
- }, null, 2),
1380
- }],
1381
- };
1382
- }
1383
- const solutions = results.slice(0, 5).map(r => ({
1384
- title: r.entry.title,
1385
- solution: r.entry.content,
1386
- relevance: r.score,
1387
- matchedTerms: r.matchedTerms,
1388
- source: r.entry.source,
1389
- }));
1390
- return {
1391
- content: [{
1392
- type: "text",
1393
- text: JSON.stringify({
1394
- error,
1395
- solutions,
1396
- totalMatches: results.length,
1397
- message: `Found ${results.length} matching patterns. Apply the most relevant solution.`,
1398
- }, null, 2),
1399
- }],
1400
- };
1401
- });
1402
- // ──────────────────────────────────────────
1403
- // Tool: stk_brain_claudemd
1404
- // ──────────────────────────────────────────
1405
- server.tool("stk_brain_claudemd", "Auto-generate a CLAUDE.md file for the current project. Analyzes the tech stack, project structure, services, and brain knowledge to create comprehensive project instructions for Claude Code.", {
1406
- projectName: z.string().optional().describe("Project name (auto-detected from stk.config.json if omitted)"),
1407
- write: z.boolean().optional().default(false).describe("Actually write the CLAUDE.md file to disk. If false, just returns the content for review."),
1408
- }, async ({ projectName, write }) => {
1409
- const config = loadConfig();
1410
- const name = projectName ?? config.name ?? "my-project";
1411
- const enabled = enabledServices(config);
1412
- // Detect project info
1413
- let gitBranch = "";
1414
- let gitRemote = "";
1415
- let packageJson = {};
1416
- try {
1417
- gitBranch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1418
- }
1419
- catch { }
1420
- try {
1421
- gitRemote = execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1422
- }
1423
- catch { }
1424
- try {
1425
- const { readFileSync } = await import("fs");
1426
- packageJson = JSON.parse(readFileSync("package.json", "utf-8"));
1427
- }
1428
- catch { }
1429
- // Detect tech stack
1430
- const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
1431
- const stack = [];
1432
- if (deps?.["next"])
1433
- stack.push("Next.js");
1434
- if (deps?.["react"])
1435
- stack.push("React");
1436
- if (deps?.["vue"])
1437
- stack.push("Vue");
1438
- if (deps?.["express"])
1439
- stack.push("Express.js");
1440
- if (deps?.["fastify"])
1441
- stack.push("Fastify");
1442
- if (deps?.["@supabase/supabase-js"])
1443
- stack.push("Supabase");
1444
- if (deps?.["stripe"])
1445
- stack.push("Stripe");
1446
- if (deps?.["prisma"] || deps?.["@prisma/client"])
1447
- stack.push("Prisma");
1448
- if (deps?.["mongoose"])
1449
- stack.push("Mongoose/MongoDB");
1450
- if (deps?.["redis"] || deps?.["ioredis"])
1451
- stack.push("Redis");
1452
- if (deps?.["tailwindcss"])
1453
- stack.push("Tailwind CSS");
1454
- if (deps?.["typescript"])
1455
- stack.push("TypeScript");
1456
- if (deps?.["zod"])
1457
- stack.push("Zod");
1458
- if (stack.length === 0 && Object.keys(deps ?? {}).length > 0) {
1459
- stack.push("Node.js");
1460
- }
1461
- // Detect file structure
1462
- let fileStructure = "";
1463
- try {
1464
- fileStructure = execSync("find . -maxdepth 3 -type f -name '*.ts' -o -name '*.js' -o -name '*.tsx' -o -name '*.jsx' -o -name '*.json' -o -name '*.css' -o -name '*.html' | grep -v node_modules | grep -v dist | grep -v .git | sort | head -40", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1465
- }
1466
- catch { }
1467
- // Build CLAUDE.md
1468
- const lines = [];
1469
- lines.push(`# ${name}\n`);
1470
- // Project overview
1471
- lines.push(`## Overview\n`);
1472
- if (packageJson.description)
1473
- lines.push(`${packageJson.description}\n`);
1474
- if (stack.length > 0)
1475
- lines.push(`**Tech Stack:** ${stack.join(", ")}\n`);
1476
- if (gitRemote)
1477
- lines.push(`**Repo:** ${gitRemote}`);
1478
- if (gitBranch)
1479
- lines.push(`**Main Branch:** ${gitBranch}`);
1480
- lines.push("");
1481
- // Services
1482
- if (enabled.length > 0) {
1483
- lines.push(`## Services\n`);
1484
- for (const svc of enabled) {
1485
- const svcName = svc.charAt(0).toUpperCase() + svc.slice(1);
1486
- lines.push(`- **${svcName}** — configured and monitored via stk`);
1487
- }
1488
- lines.push("");
1489
- }
1490
- // Project structure
1491
- if (fileStructure) {
1492
- lines.push(`## Project Structure\n`);
1493
- lines.push("```");
1494
- lines.push(fileStructure);
1495
- lines.push("```\n");
1496
- }
1497
- // Development commands
1498
- lines.push(`## Development\n`);
1499
- if (packageJson.scripts) {
1500
- lines.push("```bash");
1501
- if (packageJson.scripts.dev)
1502
- lines.push(`npm run dev # Start dev server`);
1503
- if (packageJson.scripts.build)
1504
- lines.push(`npm run build # Build for production`);
1505
- if (packageJson.scripts.test)
1506
- lines.push(`npm test # Run tests`);
1507
- if (packageJson.scripts.start)
1508
- lines.push(`npm start # Start production server`);
1509
- lines.push("```\n");
1510
- }
1511
- // stk tools available
1512
- lines.push(`## stk Tools Available\n`);
1513
- lines.push(`This project uses stk for infrastructure monitoring. Available commands:\n`);
1514
- lines.push(`- \`stk_health\` — Check service health`);
1515
- lines.push(`- \`stk_status\` — Full project overview`);
1516
- lines.push(`- \`stk_logs\` — Production logs`);
1517
- lines.push(`- \`stk_db\` — Query database`);
1518
- lines.push(`- \`stk_alerts\` — Check for problems`);
1519
- lines.push(`- \`stk_deploy\` — Deploy to production`);
1520
- lines.push(`- \`stk_brain_search\` — Search knowledge base`);
1521
- lines.push(`- \`stk_brain_learn\` — Save patterns for future use`);
1522
- lines.push("");
1523
- // Conventions
1524
- lines.push(`## Conventions\n`);
1525
- if (deps?.["typescript"]) {
1526
- lines.push(`- TypeScript strict mode enabled`);
1527
- }
1528
- lines.push(`- Use existing patterns in the codebase before introducing new ones`);
1529
- lines.push(`- Check stk_health before debugging production issues`);
1530
- lines.push(`- Use stk_brain_search to find how other projects solve similar problems`);
1531
- lines.push(`- Save useful patterns with stk_brain_learn for future reference`);
1532
- lines.push("");
1533
- // Environment
1534
- lines.push(`## Environment Variables\n`);
1535
- lines.push(`Environment variables are managed via \`.env\` files. See \`.env.example\` for required variables.`);
1536
- if (enabled.includes("vercel"))
1537
- lines.push(`Use \`stk_env_sync\` to compare local vs remote env vars.`);
1538
- lines.push("");
1539
- const content = lines.join("\n");
1540
- if (write) {
1541
- const { writeFileSync } = await import("fs");
1542
- writeFileSync("CLAUDE.md", content);
1543
- return {
1544
- content: [{
1545
- type: "text",
1546
- text: JSON.stringify({ written: true, path: "CLAUDE.md", lines: content.split("\n").length }, null, 2),
1547
- }],
1548
- };
1549
- }
1550
- return {
1551
- content: [{
1552
- type: "text",
1553
- text: JSON.stringify({ preview: true, content, note: "Call again with write: true to save to disk" }, null, 2),
1554
- }],
1555
- };
1556
- });
1557
- // Helper
1558
- function detectGitHubRepo() {
1559
- try {
1560
- const url = execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1561
- const match = url.match(/github\.com[:/]([^/]+\/[^/.]+)/);
1562
- return match?.[1] ?? null;
1563
- }
1564
- catch {
1565
- return null;
1566
- }
1567
- }
1568
- // ──────────────────────────────────────────
1569
- // Tool: stk_autofix
1570
- // ──────────────────────────────────────────
1571
- server.tool("stk_autofix", "Auto-diagnose errors from production logs. Fetches recent logs, extracts errors, and searches the brain for matching solutions. Returns errors with matched fix patterns and confidence scores.", {
1572
- provider: z.enum(["railway", "vercel"]).optional(),
1573
- lines: z.number().optional().default(50),
1574
- }, async ({ provider, lines }) => {
1575
- // Determine provider
1576
- const config = loadConfig();
1577
- const detected = provider ?? (enabledServices(config).includes("railway") ? "railway" : "vercel");
1578
- // Fetch logs using existing stk_logs logic (inline)
1579
- let logs = [];
1580
- try {
1581
- if (detected === "railway") {
1582
- const token = process.env.RAILWAY_API_TOKEN;
1583
- const projectId = process.env.RAILWAY_PROJECT_ID;
1584
- const serviceId = process.env.RAILWAY_SERVICE_ID;
1585
- if (token && projectId && serviceId) {
1586
- const envId = process.env.RAILWAY_ENVIRONMENT_ID;
1587
- const gql = `query { deployments(first:1, input:{projectId:"${projectId}",serviceId:"${serviceId}"${envId ? `,environmentId:"${envId}"` : ""}}) { edges { node { id } } } }`;
1588
- const depRes = await fetch("https://backboard.railway.app/graphql/v2", {
1589
- method: "POST",
1590
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
1591
- body: JSON.stringify({ query: gql }),
1592
- });
1593
- const depData = await depRes.json();
1594
- const depId = (depData?.data?.deployments?.edges?.[0]?.node?.id) ?? null;
1595
- if (depId) {
1596
- const logGql = `query { deploymentLogs(deploymentId:"${depId}",limit:${lines}) { timestamp message severity } }`;
1597
- const logRes = await fetch("https://backboard.railway.app/graphql/v2", {
1598
- method: "POST",
1599
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
1600
- body: JSON.stringify({ query: logGql }),
1601
- });
1602
- const logData = await logRes.json();
1603
- logs = (logData?.data?.deploymentLogs ?? []);
1604
- }
1605
- }
1606
- }
1607
- }
1608
- catch { /* log fetch failed */ }
1609
- // Filter for errors
1610
- const errorPattern = /error|exception|fail|crash|ECONNREFUSED|timeout|TypeError|ReferenceError|rejected|FATAL/i;
1611
- const errorLogs = logs.filter(l => l.severity === "error" || (l.message && errorPattern.test(l.message)));
1612
- // Deduplicate by first 100 chars
1613
- const grouped = new Map();
1614
- for (const log of errorLogs) {
1615
- const key = log.message.slice(0, 100);
1616
- const existing = grouped.get(key);
1617
- if (existing) {
1618
- existing.count++;
1619
- existing.lastSeen = log.timestamp;
1620
- }
1621
- else {
1622
- grouped.set(key, { message: log.message, count: 1, firstSeen: log.timestamp, lastSeen: log.timestamp });
1623
- }
1624
- }
1625
- // Diagnose each unique error
1626
- const errors = [];
1627
- let matchedErrors = 0;
1628
- for (const error of grouped.values()) {
1629
- const matches = brainDiagnose(error.message);
1630
- const solutions = matches.slice(0, 3).map(m => ({
1631
- title: m.entry.title,
1632
- content: m.entry.content.slice(0, 400),
1633
- confidence: Math.min(1, m.score / 10),
1634
- source: m.entry.source,
1635
- }));
1636
- if (solutions.length > 0)
1637
- matchedErrors++;
1638
- errors.push({ ...error, solutions });
1639
- }
1640
- return {
1641
- content: [{
1642
- type: "text",
1643
- text: JSON.stringify({
1644
- provider: detected,
1645
- errors,
1646
- summary: {
1647
- totalErrors: errorLogs.length,
1648
- uniqueErrors: grouped.size,
1649
- matchedErrors,
1650
- unmatchedErrors: grouped.size - matchedErrors,
1651
- },
1652
- }, null, 2),
1653
- }],
1654
- };
1655
- });
1656
- // ──────────────────────────────────────────
1657
- // Tool: stk_brain_review
1658
- // ──────────────────────────────────────────
1659
- server.tool("stk_brain_review", "Review code changes against the brain's knowledge base. Checks a git diff or PR for known gotchas per file. Use before merging PRs or after making changes.", {
1660
- diff: z.string().optional().describe("Raw git diff output"),
1661
- pr: z.number().optional().describe("GitHub PR number to review"),
1662
- }, async ({ diff, pr }) => {
1663
- let diffContent = diff ?? "";
1664
- // If PR number, fetch diff from GitHub
1665
- if (pr && !diff) {
1666
- const token = process.env.GITHUB_TOKEN;
1667
- let repo = process.env.GITHUB_REPO ?? "";
1668
- if (!repo) {
1669
- try {
1670
- repo = execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().replace(/.*github\.com[:/]/, "").replace(/\.git$/, "");
1671
- }
1672
- catch { /* */ }
1673
- }
1674
- if (repo && token) {
1675
- try {
1676
- const res = await fetch(`https://api.github.com/repos/${repo}/pulls/${pr}`, {
1677
- headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github.diff" },
1678
- });
1679
- if (res.ok)
1680
- diffContent = await res.text();
1681
- }
1682
- catch { /* */ }
1683
- }
1684
- }
1685
- // If no diff provided, get latest commit diff
1686
- if (!diffContent) {
1687
- try {
1688
- diffContent = execSync("git diff HEAD~1", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 5 });
1689
- }
1690
- catch { /* */ }
1691
- }
1692
- if (!diffContent) {
1693
- return { content: [{ type: "text", text: JSON.stringify({ error: "No diff available. Provide diff text, PR number, or ensure you're in a git repo." }) }] };
1694
- }
1695
- const results = reviewDiff(diffContent);
1696
- const filesWithWarnings = results.filter(r => r.warnings.length > 0);
1697
- return {
1698
- content: [{
1699
- type: "text",
1700
- text: JSON.stringify({
1701
- files: results,
1702
- summary: {
1703
- filesReviewed: results.length,
1704
- filesWithWarnings: filesWithWarnings.length,
1705
- totalWarnings: filesWithWarnings.reduce((s, r) => s + r.warnings.length, 0),
1706
- },
1707
- }, null, 2),
1708
- }],
1709
- };
1710
- });
1711
- // ──────────────────────────────────────────
1712
- // Tool: stk_metrics
1713
- // ──────────────────────────────────────────
1714
- server.tool("stk_metrics", "Track and analyze infrastructure metrics over time — deploy frequency, error rates, response times, uptime. Compare current performance against historical baselines to detect regressions.", {
1715
- action: z.enum(["view", "record", "compare"]).optional().default("view"),
1716
- type: z.enum(["deploy", "health_check", "error", "response_time"]).optional(),
1717
- value: z.number().optional(),
1718
- metadata: z.record(z.string(), z.string()).optional(),
1719
- days: z.number().optional().default(7),
1720
- }, async ({ action, type, value, metadata, days }) => {
1721
- if (action === "record" && type && value !== undefined) {
1722
- recordMetric(type, value, metadata ?? {});
1723
- return { content: [{ type: "text", text: JSON.stringify({ recorded: true, type, value }) }] };
1724
- }
1725
- if (action === "compare") {
1726
- const types = ["deploy", "health_check", "error", "response_time"];
1727
- const comparisons = types.map(t => ({ type: t, ...compareToBaseline(t) }));
1728
- const degraded = comparisons.filter(c => c.status === "degraded");
1729
- return {
1730
- content: [{
1731
- type: "text",
1732
- text: JSON.stringify({
1733
- comparisons,
1734
- degraded: degraded.length > 0 ? degraded : "none",
1735
- summary: `${degraded.length} metric(s) degraded`,
1736
- }, null, 2),
1737
- }],
1738
- };
1739
- }
1740
- // Default: view
1741
- return {
1742
- content: [{
1743
- type: "text",
1744
- text: JSON.stringify({
1745
- period: `${days} days`,
1746
- deploys: getDeployFrequency(days),
1747
- errors: getErrorRate(days),
1748
- uptime: getUptime(days),
1749
- recentMetrics: getMetrics(type ?? undefined, days, 20),
1750
- }, null, 2),
1751
- }],
1752
- };
1753
- });
1754
- // ──────────────────────────────────────────
1755
- // Tool: stk_secure
1756
- // ──────────────────────────────────────────
1757
- server.tool("stk_secure", "Security scan — check for exposed secrets, vulnerable dependencies, missing rate limiting, open CORS, and unprotected routes. Returns findings with severity levels and fix suggestions.", {
1758
- checks: z.array(z.enum(["secrets", "deps", "rate_limit", "cors", "auth"])).optional().describe("Specific checks to run (default: all)"),
1759
- }, async ({ checks }) => {
1760
- const findings = runAllChecks(checks);
1761
- const critical = findings.filter(f => f.level === "critical").length;
1762
- const warning = findings.filter(f => f.level === "warning").length;
1763
- const info = findings.filter(f => f.level === "info").length;
1764
- return {
1765
- content: [{
1766
- type: "text",
1767
- text: JSON.stringify({
1768
- findings,
1769
- summary: { critical, warning, info, total: findings.length },
1770
- status: critical > 0 ? "CRITICAL" : warning > 0 ? "WARNING" : "CLEAN",
1771
- }, null, 2),
1772
- }],
1773
- };
1774
- });
1775
- // ──────────────────────────────────────────
1776
- // Tool: stk_brain_team
1777
- // ──────────────────────────────────────────
1778
- server.tool("stk_brain_team", "Show team brain contributions — who learned what, from which projects, and when. Tracks knowledge sharing across team members.", {}, async () => {
1779
- const store = loadBrainStore();
1780
- const allEntries = [...store.global];
1781
- for (const proj of Object.values(store.projects))
1782
- allEntries.push(...proj.entries);
1783
- const contributors = {};
1784
- for (const entry of allEntries) {
1785
- const contributor = entry.contributor ?? "auto";
1786
- if (!contributors[contributor]) {
1787
- contributors[contributor] = { count: 0, lastActive: entry.created_at, categories: {} };
1788
- }
1789
- contributors[contributor].count++;
1790
- if (entry.created_at > contributors[contributor].lastActive) {
1791
- contributors[contributor].lastActive = entry.created_at;
1792
- }
1793
- contributors[contributor].categories[entry.category] = (contributors[contributor].categories[entry.category] || 0) + 1;
1794
- }
1795
- return {
1796
- content: [{
1797
- type: "text",
1798
- text: JSON.stringify({
1799
- totalEntries: allEntries.length,
1800
- contributors,
1801
- projects: Object.entries(store.projects).map(([name, p]) => ({
1802
- name, entries: p.entries.length, ingestedAt: p.ingestedAt,
1803
- })),
1804
- }, null, 2),
1805
- }],
1806
- };
1807
- });
19
+ version: "0.7.0",
20
+ });
21
+ registerInfraTools(server);
22
+ registerOpsTools(server);
23
+ registerDataTools(server);
24
+ registerBrainTools(server);
25
+ registerGithubTools(server);
26
+ registerSecurityTools(server);
1808
27
  // Start
1809
28
  async function main() {
1810
29
  const transport = new StdioServerTransport();