kickload-watcher-mcp 0.1.0 → 0.1.2

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/index.js CHANGED
@@ -1,270 +1,266 @@
1
- #!/usr/bin/env node
2
-
3
- // ── Global error guards — must be first ──────────────────────────────────────
4
- process.on("uncaughtException", (err) => {
5
- console.error("\n❌ Uncaught error:", err.message);
6
- if (process.env.LOG_LEVEL === "debug") console.error(err.stack);
7
- process.exit(1);
8
- });
9
-
10
- process.on("unhandledRejection", (reason) => {
11
- const msg = reason instanceof Error ? reason.message : String(reason);
12
- console.error("\n❌ Unhandled rejection:", msg);
13
- if (process.env.LOG_LEVEL === "debug" && reason instanceof Error) console.error(reason.stack);
14
- process.exit(1);
15
- });
16
-
17
- // ── Node version check ────────────────────────────────────────────────────────
18
- const nodeMajor = parseInt(process.versions.node.split(".")[0]);
19
- if (nodeMajor < 18) {
20
- console.error(`\n❌ Node.js 18+ required. You have ${process.versions.node}.`);
21
- console.error(" Download: https://nodejs.org\n");
22
- process.exit(1);
23
- }
24
-
25
- // ── First-run setup (writes .env if missing, reloads env in-process) ─────────
26
- // STARTUP FIX: runFirstRunSetup no longer exits — continues execution after writing .env
27
- import { runFirstRunSetup, ensureGitignore } from "./setup.js";
28
- await runFirstRunSetup();
29
-
30
- // ── Load env ──────────────────────────────────────────────────────────────────
31
- import dotenv from "dotenv";
32
- dotenv.config();
33
-
34
- // ── All other imports depend on env being loaded ──────────────────────────────
35
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
36
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
37
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
38
-
39
- import { config, validateConfig } from "./config.js";
40
- import { initEmailSender, sendResultEmail, verifyEmailService } from "./email-sender.js";
41
- import { watchIdentityMap, getEmailForUser } from "./identity-map.js";
42
- import { startCompliancePoller, onNewSession } from "./compliance-poller.js";
43
- import { startFileWatcher, onNewApiFile } from "./file-watcher.js";
44
- import { runPhase1Pipeline, handleFileEvent,
45
- handleComplianceSession, handleGithubEvent } from "./pipeline.js";
46
- import { generateTestPrompt } from "./test-generator.js";
47
- import { startGithubWebhookServer, onGithubPR } from "./github-webhook.js";
48
- import { ensureNgrokInstalled, stopNgrok } from "./ngrok-manager.js";
49
- import { createLogger } from "./logger.js";
50
-
51
- const logger = createLogger("main");
52
-
53
- // ── Config validation (exits on missing required fields) ─────────────────────
54
- validateConfig();
55
- ensureGitignore();
56
-
57
- // ── PROBLEM 2 — Email strict mode: init + verify before anything else ─────────
58
- console.log("\n🔧 Initializing subsystems...");
59
-
60
- try {
61
- initEmailSender();
62
- } catch (err) {
63
- console.error(`\n❌ Email initialization failed: ${err.message}`);
64
- console.error(" Check EMAIL_PROVIDER and related settings in .env\n");
65
- process.exit(1);
66
- }
67
-
68
- // Verify SMTP credentials exits if broken
69
- await verifyEmailService();
70
-
71
- // ── ngrok binary pre-fetch (only if auth token is configured) ─────────────────
72
- // PROBLEM 1: We no longer check NGROK_ENABLED at startup for localhost backends
73
- // (pipeline.js handles that automatically). We still pre-install the binary
74
- // if a token is present, so the first tunnel starts faster.
75
- if (config.ngrok.authToken) {
76
- logger.info("Pre-fetching ngrok binary...");
77
- try {
78
- await ensureNgrokInstalled();
79
- logger.info(" ngrok ready.");
80
- } catch (err) {
81
- // Non-fatal — pipeline will handle the failure gracefully
82
- logger.warn(`ngrok pre-install failed: ${err.message}`);
83
- logger.warn("Localhost backends will attempt ngrok on first use.");
84
- }
85
- } else {
86
- logger.warn("NGROK_AUTHTOKEN not set ngrok will attempt unauthenticated tunnels for localhost backends.");
87
- logger.warn("Add NGROK_AUTHTOKEN to .env to fix: https://dashboard.ngrok.com");
88
- }
89
-
90
- // ── Init remaining subsystems ────────────────────────────────────────────────
91
- watchIdentityMap();
92
-
93
- onNewSession(handleComplianceSession);
94
- onNewApiFile(handleFileEvent);
95
-
96
- if (config.triggerMode === "claudecode" || config.triggerMode === "both") {
97
- startCompliancePoller();
98
- startFileWatcher();
99
- }
100
-
101
- if (config.triggerMode === "github" || config.triggerMode === "both") {
102
- onGithubPR(handleGithubEvent);
103
- startGithubWebhookServer();
104
- }
105
-
106
- // ── Graceful shutdown ─────────────────────────────────────────────────────────
107
- const shutdown = (sig) => {
108
- console.log(`\n🛑 ${sig} — shutting down`);
109
- stopNgrok();
110
- process.exit(0);
111
- };
112
- process.on("SIGINT", () => shutdown("SIGINT"));
113
- process.on("SIGTERM", () => shutdown("SIGTERM"));
114
-
115
- // ── MCP Server ────────────────────────────────────────────────────────────────
116
- const server = new Server(
117
- { name: "kickload-watcher", version: "1.1.0" },
118
- { capabilities: { tools: {} } }
119
- );
120
-
121
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
122
- tools: [
123
- {
124
- name: "run_kickload_test",
125
- description: "Trigger a KickLoad performance test for an API endpoint.",
126
- inputSchema: {
127
- type: "object",
128
- properties: {
129
- endpoint: { type: "string", description: "API endpoint path, e.g. /api/checkout" },
130
- description: { type: "string", description: "What the endpoint does" },
131
- developer_email: { type: "string", description: "Email to send results to" },
132
- num_threads: { type: "number", description: "Concurrent users" },
133
- loop_count: { type: "number", description: "Loops per user" },
134
- ramp_time: { type: "number", description: "Ramp-up seconds" },
135
- },
136
- required: ["endpoint", "developer_email"],
137
- },
138
- },
139
- {
140
- name: "get_watcher_status",
141
- description: "Check the current watcher status.",
142
- inputSchema: { type: "object", properties: {}, required: [] },
143
- },
144
- {
145
- name: "lookup_developer",
146
- description: "Look up the email for a Claude user ID from users.json.",
147
- inputSchema: {
148
- type: "object",
149
- properties: { claude_user_id: { type: "string" } },
150
- required: ["claude_user_id"],
151
- },
152
- },
153
- {
154
- name: "send_test_email",
155
- description: "Send a test email to verify email configuration.",
156
- inputSchema: {
157
- type: "object",
158
- properties: { to_email: { type: "string" } },
159
- required: ["to_email"],
160
- },
161
- },
162
- ],
163
- }));
164
-
165
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
166
- const { name, arguments: args } = request.params;
167
-
168
- if (name === "run_kickload_test") {
169
- const { endpoint, description, developer_email } = args;
170
- try {
171
- logger.info(`Manual test: ${endpoint}`);
172
- let prompt = `Load test the ${endpoint} endpoint for performance and stability.`;
173
- if (description) {
174
- const p = await generateTestPrompt({
175
- fileContent: `// ${endpoint}\n// ${description}`,
176
- detectedEndpoints: [endpoint],
177
- originalPrompt: description,
178
- fileName: "mcp-manual",
179
- });
180
- prompt = p.testPrompt;
181
- }
182
- const result = await runPhase1Pipeline({
183
- fileName: "mcp-manual",
184
- fileContent: `// ${endpoint}`,
185
- detectedEndpoints: [endpoint],
186
- originalPrompt: prompt,
187
- devEmail: developer_email,
188
- userId: null,
189
- source: "mcp_manual",
190
- });
191
- return {
192
- content: [{ type: "text", text: JSON.stringify({
193
- success: result.success,
194
- status: result.summary?.passed ? "PASS" : "FAIL",
195
- averageLatency: result.summary?.averageLatency,
196
- errorPercentage: result.summary?.errorPercentage,
197
- throughput: result.summary?.throughput,
198
- downloadUrl: result.downloadUrl,
199
- emailSentTo: developer_email,
200
- }, null, 2) }],
201
- };
202
- } catch (err) {
203
- logger.error(`run_kickload_test failed: ${err.message}`);
204
- return {
205
- content: [{ type: "text", text: JSON.stringify({ success: false, error: err.message }) }],
206
- isError: true,
207
- };
208
- }
209
- }
210
-
211
- if (name === "get_watcher_status") {
212
- return {
213
- content: [{ type: "text", text: JSON.stringify({
214
- status: "running",
215
- version: "1.1.0",
216
- triggerMode: config.triggerMode,
217
- watchPaths: config.watcher.watchPaths,
218
- emailProvider: config.email.provider,
219
- kickloadBaseUrl: config.kickload.baseUrl,
220
- ngrokAutoMode: "localhost backends auto-tunneled",
221
- ngrokTokenSet: !!config.ngrok.authToken,
222
- compliancePolling: !!config.anthropic.complianceApiKey,
223
- }, null, 2) }],
224
- };
225
- }
226
-
227
- if (name === "lookup_developer") {
228
- const email = getEmailForUser(args.claude_user_id);
229
- return {
230
- content: [{ type: "text", text: JSON.stringify({
231
- claudeUserId: args.claude_user_id,
232
- email: email || null,
233
- found: !!email,
234
- }, null, 2) }],
235
- };
236
- }
237
-
238
- if (name === "send_test_email") {
239
- try {
240
- await sendResultEmail({
241
- toEmail: args.to_email,
242
- endpoint: "/api/test",
243
- passed: true,
244
- summary: { averageLatency: 123, errorPercentage: 0.5, throughput: 4500 },
245
- downloadUrl: null,
246
- pdfFilename: "test.pdf",
247
- durationMs: 45000,
248
- testPrompt: "Test email.",
249
- });
250
- return { content: [{ type: "text", text: JSON.stringify({ success: true }) }] };
251
- } catch (err) {
252
- return {
253
- content: [{ type: "text", text: JSON.stringify({ success: false, error: err.message }) }],
254
- isError: true,
255
- };
256
- }
257
- }
258
-
259
- return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
260
- });
261
-
262
- const transport = new StdioServerTransport();
263
- await server.connect(transport);
264
-
265
- console.log("\n✅ KickLoad Watcher running");
266
- console.log(` Mode : ${config.triggerMode}`);
267
- console.log(` KickLoad: ${config.kickload.baseUrl}`);
268
- console.log(` ngrok : auto-mode (localhost backends tunneled automatically)`);
269
- console.log(` Email : ${config.email.provider} ✓`);
270
- console.log(` Watching: ${config.watcher.watchPaths.join(", ")}\n`);
1
+ #!/usr/bin/env node
2
+
3
+ // ── Global error guards — must be first ──────────────────────────────────────
4
+ process.on("uncaughtException", (err) => {
5
+ console.error("\n❌ Uncaught error:", err.message);
6
+ if (process.env.LOG_LEVEL === "debug") console.error(err.stack);
7
+ process.exit(1);
8
+ });
9
+
10
+ process.on("unhandledRejection", (reason) => {
11
+ const msg = reason instanceof Error ? reason.message : String(reason);
12
+ console.error("\n❌ Unhandled rejection:", msg);
13
+ if (process.env.LOG_LEVEL === "debug" && reason instanceof Error) console.error(reason.stack);
14
+ process.exit(1);
15
+ });
16
+
17
+ // ── Node version check ────────────────────────────────────────────────────────
18
+ const nodeMajor = parseInt(process.versions.node.split(".")[0]);
19
+ if (nodeMajor < 18) {
20
+ console.error(`\n❌ Node.js 18+ required. You have ${process.versions.node}.`);
21
+ console.error(" Download: https://nodejs.org\n");
22
+ process.exit(1);
23
+ }
24
+
25
+ // ── First-run setup (writes .env if missing, reloads env in-process) ─────────
26
+ // STARTUP FIX: runFirstRunSetup no longer exits — continues execution after writing .env
27
+ import { runFirstRunSetup, ensureGitignore } from "./setup.js";
28
+ await runFirstRunSetup();
29
+
30
+ const { default: dotenv } = await import("dotenv");
31
+ dotenv.config();
32
+ // ── All other imports depend on env being loaded ──────────────────────────────
33
+ const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
34
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
35
+ const { CallToolRequestSchema, ListToolsRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
36
+ const { config, validateConfig } = await import("./config.js");
37
+ const { initEmailSender, verifyEmailService, sendResultEmail } = await import("./email-sender.js");
38
+ const { watchIdentityMap, getEmailForUser } = await import("./identity-map.js");
39
+ const { startCompliancePoller, onNewSession } = await import("./compliance-poller.js");
40
+ const { startFileWatcher, onNewApiFile } = await import("./file-watcher.js");
41
+ const { runPhase1Pipeline, handleFileEvent, handleComplianceSession, handleGithubEvent } = await import("./pipeline.js");
42
+ const { generateTestPrompt } = await import("./test-generator.js");
43
+ const { startGithubWebhookServer, onGithubPR } = await import("./github-webhook.js");
44
+ const { ensureNgrokInstalled, stopNgrok } = await import("./ngrok-manager.js");
45
+ const { createLogger } = await import("./logger.js");
46
+
47
+ const logger = createLogger("main");
48
+
49
+ // ── Config validation (exits on missing required fields) ─────────────────────
50
+ validateConfig();
51
+ ensureGitignore();
52
+
53
+ // ── PROBLEM 2 Email strict mode: init + verify before anything else ─────────
54
+ console.log("\n🔧 Initializing subsystems...");
55
+
56
+ try {
57
+ initEmailSender();
58
+ } catch (err) {
59
+ console.error(`\n❌ Email initialization failed: ${err.message}`);
60
+ console.error(" Check EMAIL_PROVIDER and related settings in .env\n");
61
+ process.exit(1);
62
+ }
63
+
64
+ // Verify SMTP credentials exits if broken
65
+ await verifyEmailService();
66
+
67
+ // ── ngrok binary pre-fetch (only if auth token is configured) ─────────────────
68
+ // PROBLEM 1: We no longer check NGROK_ENABLED at startup for localhost backends
69
+ // (pipeline.js handles that automatically). We still pre-install the binary
70
+ // if a token is present, so the first tunnel starts faster.
71
+ if (config.ngrok.authToken) {
72
+ logger.info("Pre-fetching ngrok binary...");
73
+ try {
74
+ await ensureNgrokInstalled();
75
+ logger.info("✅ ngrok ready.");
76
+ } catch (err) {
77
+ // Non-fatal — pipeline will handle the failure gracefully
78
+ logger.warn(`ngrok pre-install failed: ${err.message}`);
79
+ logger.warn("Localhost backends will attempt ngrok on first use.");
80
+ }
81
+ } else {
82
+ logger.warn("NGROK_AUTHTOKEN not set — ngrok will attempt unauthenticated tunnels for localhost backends.");
83
+ logger.warn("Add NGROK_AUTHTOKEN to .env to fix: https://dashboard.ngrok.com");
84
+ }
85
+
86
+ // ── Init remaining subsystems ────────────────────────────────────────────────
87
+ watchIdentityMap();
88
+
89
+ onNewSession(handleComplianceSession);
90
+ onNewApiFile(handleFileEvent);
91
+
92
+ if (config.triggerMode === "claudecode" || config.triggerMode === "both") {
93
+ startCompliancePoller();
94
+ startFileWatcher();
95
+ }
96
+
97
+ if (config.triggerMode === "github" || config.triggerMode === "both") {
98
+ onGithubPR(handleGithubEvent);
99
+ startGithubWebhookServer();
100
+ }
101
+
102
+ // ── Graceful shutdown ─────────────────────────────────────────────────────────
103
+ const shutdown = (sig) => {
104
+ console.log(`\n🛑 ${sig} — shutting down`);
105
+ stopNgrok();
106
+ process.exit(0);
107
+ };
108
+ process.on("SIGINT", () => shutdown("SIGINT"));
109
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
110
+
111
+ // ── MCP Server ────────────────────────────────────────────────────────────────
112
+ const server = new Server(
113
+ { name: "kickload-watcher-mcp", version: "0.1.1" },
114
+ { capabilities: { tools: {} } }
115
+ );
116
+
117
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
118
+ tools: [
119
+ {
120
+ name: "run_kickload_test",
121
+ description: "Trigger a KickLoad performance test for an API endpoint.",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: {
125
+ endpoint: { type: "string", description: "API endpoint path, e.g. /api/checkout" },
126
+ description: { type: "string", description: "What the endpoint does" },
127
+ developer_email: { type: "string", description: "Email to send results to" },
128
+ num_threads: { type: "number", description: "Concurrent users" },
129
+ loop_count: { type: "number", description: "Loops per user" },
130
+ ramp_time: { type: "number", description: "Ramp-up seconds" },
131
+ },
132
+ required: ["endpoint", "developer_email"],
133
+ },
134
+ },
135
+ {
136
+ name: "get_watcher_status",
137
+ description: "Check the current watcher status.",
138
+ inputSchema: { type: "object", properties: {}, required: [] },
139
+ },
140
+ {
141
+ name: "lookup_developer",
142
+ description: "Look up the email for a Claude user ID from users.json.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: { claude_user_id: { type: "string" } },
146
+ required: ["claude_user_id"],
147
+ },
148
+ },
149
+ {
150
+ name: "send_test_email",
151
+ description: "Send a test email to verify email configuration.",
152
+ inputSchema: {
153
+ type: "object",
154
+ properties: { to_email: { type: "string" } },
155
+ required: ["to_email"],
156
+ },
157
+ },
158
+ ],
159
+ }));
160
+
161
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
162
+ const { name, arguments: args } = request.params;
163
+
164
+ if (name === "run_kickload_test") {
165
+ const { endpoint, description, developer_email } = args;
166
+ try {
167
+ logger.info(`Manual test: ${endpoint}`);
168
+ let prompt = `Load test the ${endpoint} endpoint for performance and stability.`;
169
+ if (description) {
170
+ const p = await generateTestPrompt({
171
+ fileContent: `// ${endpoint}\n// ${description}`,
172
+ detectedEndpoints: [endpoint],
173
+ originalPrompt: description,
174
+ fileName: "mcp-manual",
175
+ });
176
+ prompt = p.testPrompt;
177
+ }
178
+ const result = await runPhase1Pipeline({
179
+ fileName: "mcp-manual",
180
+ fileContent: `// ${endpoint}`,
181
+ detectedEndpoints: [endpoint],
182
+ originalPrompt: prompt,
183
+ devEmail: developer_email,
184
+ userId: null,
185
+ source: "mcp_manual",
186
+ });
187
+ return {
188
+ content: [{ type: "text", text: JSON.stringify({
189
+ success: result.success,
190
+ status: result.summary?.passed ? "PASS" : "FAIL",
191
+ averageLatency: result.summary?.averageLatency,
192
+ errorPercentage: result.summary?.errorPercentage,
193
+ throughput: result.summary?.throughput,
194
+ downloadUrl: result.downloadUrl,
195
+ emailSentTo: developer_email,
196
+ }, null, 2) }],
197
+ };
198
+ } catch (err) {
199
+ logger.error(`run_kickload_test failed: ${err.message}`);
200
+ return {
201
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: err.message }) }],
202
+ isError: true,
203
+ };
204
+ }
205
+ }
206
+
207
+ if (name === "get_watcher_status") {
208
+ return {
209
+ content: [{ type: "text", text: JSON.stringify({
210
+ status: "running",
211
+ version: "1.1.0",
212
+ triggerMode: config.triggerMode,
213
+ watchPaths: config.watcher.watchPaths,
214
+ emailProvider: config.email.provider,
215
+ kickloadBaseUrl: config.kickload.baseUrl,
216
+ ngrokAutoMode: "localhost backends auto-tunneled",
217
+ ngrokTokenSet: !!config.ngrok.authToken,
218
+ compliancePolling: !!config.anthropic.complianceApiKey,
219
+ }, null, 2) }],
220
+ };
221
+ }
222
+
223
+ if (name === "lookup_developer") {
224
+ const email = getEmailForUser(args.claude_user_id);
225
+ return {
226
+ content: [{ type: "text", text: JSON.stringify({
227
+ claudeUserId: args.claude_user_id,
228
+ email: email || null,
229
+ found: !!email,
230
+ }, null, 2) }],
231
+ };
232
+ }
233
+
234
+ if (name === "send_test_email") {
235
+ try {
236
+ await sendResultEmail({
237
+ toEmail: args.to_email,
238
+ endpoint: "/api/test",
239
+ passed: true,
240
+ summary: { averageLatency: 123, errorPercentage: 0.5, throughput: 4500 },
241
+ downloadUrl: null,
242
+ pdfFilename: "test.pdf",
243
+ durationMs: 45000,
244
+ testPrompt: "Test email.",
245
+ });
246
+ return { content: [{ type: "text", text: JSON.stringify({ success: true }) }] };
247
+ } catch (err) {
248
+ return {
249
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: err.message }) }],
250
+ isError: true,
251
+ };
252
+ }
253
+ }
254
+
255
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
256
+ });
257
+
258
+ const transport = new StdioServerTransport();
259
+ await server.connect(transport);
260
+
261
+ console.log("\n✅ KickLoad Watcher running");
262
+ console.log(` Mode : ${config.triggerMode}`);
263
+ console.log(` KickLoad: ${config.kickload.baseUrl}`);
264
+ console.log(` ngrok : auto-mode (localhost backends tunneled automatically)`);
265
+ console.log(` Email : ${config.email.provider} ✓`);
266
+ console.log(` Watching: ${config.watcher.watchPaths.join(", ")}\n`);