kickload-watcher-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # KickLoad Watcher MCP
2
+
3
+ Automated API performance testing for Claude Enterprise teams.
4
+
5
+ When a developer writes an API using Claude Code, this watcher **automatically**:
6
+ 1. Detects the new endpoint
7
+ 2. Generates smart load test parameters using Claude
8
+ 3. Runs the full test pipeline via KickLoad
9
+ 4. Emails the developer their results
10
+
11
+ **No GitHub required. Works directly with Claude Code CLI.**
12
+
13
+ ---
14
+
15
+ ## How It Works
16
+
17
+ ```
18
+ Developer writes API with Claude Code
19
+
20
+ Watcher detects new session (Compliance API) + saved file (file watcher)
21
+
22
+ Claude generates optimal test parameters for the endpoint
23
+
24
+ KickLoad runs: generate JMX → run test → analyze JTL → return PDF
25
+
26
+ Developer receives email with PASS/FAIL + latency + error rate + PDF link
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Prerequisites
32
+
33
+ | Requirement | Where to Get It |
34
+ |---|---|
35
+ | Claude Enterprise plan | Required for Compliance API |
36
+ | KickLoad API token | kickload.neeyatai.com → API's → Generate API Key |
37
+ | Anthropic API key | platform.claude.com → API Keys |
38
+ | Node.js 18+ | nodejs.org |
39
+
40
+ ---
41
+
42
+ ## Setup (3 Steps)
43
+
44
+ ### Step 1 — Install
45
+
46
+ ```bash
47
+ git clone <this-repo>
48
+ cd kickload-watcher-mcp
49
+ npm install
50
+ ```
51
+
52
+ ### Step 2 — Configure
53
+
54
+ ```bash
55
+ cp .env.example .env
56
+ ```
57
+
58
+ Fill in your `.env`:
59
+
60
+ ```env
61
+ ANTHROPIC_API_KEY=sk-ant-api03-...
62
+ ANTHROPIC_COMPLIANCE_API_KEY= # From Anthropic Console → Data and Privacy
63
+ KICKLOAD_API_TOKEN=kl-... # From kickload.neeyatai.com → API's
64
+ EMAIL_PROVIDER=smtp
65
+ SMTP_HOST=smtp.gmail.com
66
+ SMTP_USER=your@gmail.com
67
+ SMTP_PASS=your-app-password
68
+ EMAIL_FROM_ADDRESS=your@gmail.com
69
+ WATCH_PATHS=/home/user/projects
70
+ ```
71
+
72
+ ### Step 3 — Map Developers
73
+
74
+ Edit `users.json` to map Claude user IDs to emails:
75
+
76
+ ```json
77
+ {
78
+ "usr_abc123": "john@company.com",
79
+ "usr_xyz789": "jane@company.com"
80
+ }
81
+ ```
82
+
83
+ Find Claude user IDs in: **Anthropic Console → Members**
84
+
85
+ ---
86
+
87
+ ## Add to Claude Code
88
+
89
+ **Option A — Claude Desktop** (`claude_desktop_config.json`):
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "kickload-watcher": {
95
+ "command": "npx",
96
+ "args": ["-y", "@kickload/watcher-mcp"],
97
+ "env": {
98
+ "KICKLOAD_API_TOKEN": "your-token",
99
+ "ANTHROPIC_API_KEY": "your-key",
100
+ "EMAIL_PROVIDER": "smtp",
101
+ "SMTP_HOST": "smtp.gmail.com",
102
+ "SMTP_USER": "your@gmail.com",
103
+ "SMTP_PASS": "your-password",
104
+ "EMAIL_FROM_ADDRESS": "your@gmail.com",
105
+ "WATCH_PATHS": "/home/user/projects"
106
+ }
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ **Option B — Claude Code CLI**:
113
+
114
+ ```bash
115
+ claude mcp add kickload-watcher --type stdio --command "node /path/to/index.js"
116
+ ```
117
+
118
+ **Option C — Run directly**:
119
+
120
+ ```bash
121
+ node index.js
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Enable Compliance API (Enterprise Required)
127
+
128
+ 1. Go to **platform.claude.com**
129
+ 2. Organization Settings → Data and Privacy
130
+ 3. Enable Compliance API
131
+ 4. Create a key and add it to `.env` as `ANTHROPIC_COMPLIANCE_API_KEY`
132
+
133
+ > Only Primary Owners can enable the Compliance API.
134
+
135
+ ---
136
+
137
+ ## MCP Tools Available to Claude
138
+
139
+ Once connected, Claude can call these tools directly:
140
+
141
+ | Tool | What It Does |
142
+ |---|---|
143
+ | `run_kickload_test` | Manually trigger a test for any endpoint |
144
+ | `get_test_status` | Check watcher status and configuration |
145
+ | `lookup_developer` | Find email for a Claude user ID |
146
+ | `send_test_email` | Verify email configuration |
147
+
148
+ **Example — Claude can say:**
149
+ > "Run a load test on /api/checkout and email results to dev@company.com"
150
+
151
+ ---
152
+
153
+ ## Email Providers
154
+
155
+ | Provider | Config |
156
+ |---|---|
157
+ | SMTP (Gmail) | `EMAIL_PROVIDER=smtp` + SMTP_* vars |
158
+ | SendGrid | `EMAIL_PROVIDER=sendgrid` + `SENDGRID_API_KEY` |
159
+ | AWS SES | `EMAIL_PROVIDER=ses` + AWS_* vars |
160
+
161
+ **Gmail tip**: Use an App Password, not your account password.
162
+ Generate at: Google Account → Security → 2-Step Verification → App passwords
163
+
164
+ ---
165
+
166
+ ## Project Structure
167
+
168
+ ```
169
+ kickload-watcher-mcp/
170
+ ├── index.js ← MCP server + tool handlers
171
+ ├── orchestrator.js ← Main pipeline coordinator
172
+ ├── compliance-poller.js ← Anthropic Compliance API watcher
173
+ ├── file-watcher.js ← Filesystem watcher for saved code
174
+ ├── test-generator.js ← Claude API test parameter generator
175
+ ├── kickload-runner.js ← KickLoad API integration (/cra-line)
176
+ ├── email-sender.js ← Email delivery (SMTP/SendGrid/SES)
177
+ ├── identity-map.js ← Claude user ID → email mapping
178
+ ├── config.js ← All configuration
179
+ ├── users.json ← Developer identity map (you fill this)
180
+ ├── .env.example ← Environment variable template
181
+ └── package.json
182
+ ```
183
+
184
+ ---
185
+
186
+ ## What the Email Looks Like
187
+
188
+ ```
189
+ Subject: [KickLoad Watcher] ✅ PASS — /api/checkout tested
190
+
191
+ Hi John,
192
+
193
+ Your API endpoint was automatically tested by KickLoad Watcher.
194
+
195
+ STATUS: ✅ PASS
196
+ ENDPOINT: /api/checkout
197
+ TESTED AT: 2 Apr 2026, 14:32 IST
198
+
199
+ RESULTS:
200
+ Average Latency: 123ms
201
+ Error Rate: 0.5%
202
+ Throughput: 4,500 req/s
203
+ Test Duration: 47s
204
+
205
+ 📄 Download Full Report → [link expires in 24hrs]
206
+
207
+ — KickLoad Watcher | Automated by NeeyatAI
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Troubleshooting
213
+
214
+ **No email received?**
215
+ - Run `send_test_email` MCP tool to verify email config
216
+ - Check `users.json` has the correct Claude user ID mapped
217
+ - Gmail: ensure App Password is used, not account password
218
+
219
+ **Compliance API not working?**
220
+ - Verify Enterprise plan is active
221
+ - Only Primary Owners can enable Compliance API
222
+ - File watcher still works without it
223
+
224
+ **Test not triggering?**
225
+ - Check `WATCH_PATHS` includes your project directories
226
+ - Ensure files have supported extensions (.js, .ts, .py, etc.)
227
+ - Check console logs for "API code detected" messages
228
+
229
+ ---
230
+
231
+ ## License
232
+
233
+ MIT © NeeyatAI
@@ -0,0 +1,215 @@
1
+ // compliance-poller.js — Polls Anthropic Compliance API for new Claude Code sessions
2
+ // Enterprise plan required. Enable at:
3
+ // platform.claude.com → Organization Settings → Data and Privacy → Compliance API
4
+
5
+ import { config } from "./config.js";
6
+
7
+ let lastEventTimestamp = null;
8
+ let pollingInterval = null;
9
+ const sessionCallbacks = [];
10
+
11
+ /**
12
+ * Register a callback to be called when a new API-related session is detected.
13
+ * Callback receives: { userId, sessionId, generatedCode, prompt, timestamp }
14
+ */
15
+ export function onNewSession(callback) {
16
+ sessionCallbacks.push(callback);
17
+ }
18
+
19
+ /**
20
+ * Start polling the Compliance API on the configured interval.
21
+ */
22
+ export function startCompliancePoller() {
23
+ if (!config.anthropic.complianceApiKey) {
24
+ console.warn("⚠️ ANTHROPIC_COMPLIANCE_API_KEY not set.");
25
+ console.warn(" Compliance API polling disabled.");
26
+ console.warn(" File watcher will be the only trigger.");
27
+ return;
28
+ }
29
+
30
+ console.log(
31
+ `🔍 Compliance API poller started (every ${config.anthropic.pollIntervalMs / 1000}s)`
32
+ );
33
+
34
+ // Set initial timestamp to now so we only pick up new events
35
+ lastEventTimestamp = new Date().toISOString();
36
+
37
+ pollingInterval = setInterval(async () => {
38
+ try {
39
+ await pollComplianceEvents();
40
+ } catch (err) {
41
+ console.error("❌ Compliance API poll failed:", err.message);
42
+ }
43
+ }, config.anthropic.pollIntervalMs);
44
+
45
+ // Run immediately on start
46
+ pollComplianceEvents().catch((err) =>
47
+ console.error("❌ Initial compliance poll failed:", err.message)
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Stop the compliance poller.
53
+ */
54
+ export function stopCompliancePoller() {
55
+ if (pollingInterval) {
56
+ clearInterval(pollingInterval);
57
+ pollingInterval = null;
58
+ console.log("⏹ Compliance API poller stopped.");
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Fetch recent events from the Compliance API and filter for API-related code.
64
+ */
65
+ async function pollComplianceEvents() {
66
+ const url = new URL(`${config.anthropic.complianceBaseUrl}/organizations/audit-log`);
67
+
68
+ if (lastEventTimestamp) {
69
+ url.searchParams.set("after", lastEventTimestamp);
70
+ }
71
+
72
+ url.searchParams.set("limit", "50");
73
+
74
+ const response = await fetch(url.toString(), {
75
+ method: "GET",
76
+ headers: {
77
+ "anthropic-organization-key": config.anthropic.complianceApiKey,
78
+ "Content-Type": "application/json",
79
+ },
80
+ });
81
+
82
+ if (!response.ok) {
83
+ const body = await response.text();
84
+ throw new Error(`Compliance API ${response.status}: ${body}`);
85
+ }
86
+
87
+ const data = await response.json();
88
+ const events = data.events || data.data || [];
89
+
90
+ if (events.length === 0) return;
91
+
92
+ // Update timestamp to latest event so we don't re-process
93
+ const latest = events[events.length - 1];
94
+ if (latest.created_at) {
95
+ lastEventTimestamp = latest.created_at;
96
+ }
97
+
98
+ // Filter events that contain API-related code
99
+ for (const event of events) {
100
+ const session = extractApiSession(event);
101
+ if (session) {
102
+ console.log(`📡 New API session detected from user: ${session.userId}`);
103
+ for (const cb of sessionCallbacks) {
104
+ try {
105
+ await cb(session);
106
+ } catch (err) {
107
+ console.error("❌ Session callback error:", err.message);
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Extract relevant session data from a compliance event.
116
+ * Returns null if the event doesn't contain API-related code.
117
+ */
118
+ function extractApiSession(event) {
119
+ try {
120
+ // Compliance API event structure
121
+ const userId =
122
+ event.actor?.id ||
123
+ event.user_id ||
124
+ event.actor?.user_id ||
125
+ null;
126
+
127
+ const sessionId = event.id || event.session_id || null;
128
+ const timestamp = event.created_at || new Date().toISOString();
129
+
130
+ // Get message content — structure varies by event type
131
+ const messages =
132
+ event.payload?.messages ||
133
+ event.messages ||
134
+ event.content?.messages ||
135
+ [];
136
+
137
+ // Look through messages for generated code with API endpoints
138
+ let generatedCode = null;
139
+ let userPrompt = null;
140
+
141
+ for (const msg of messages) {
142
+ // Capture the user's original prompt
143
+ if (msg.role === "user" && typeof msg.content === "string") {
144
+ userPrompt = msg.content;
145
+ }
146
+
147
+ // Look for assistant response containing API code
148
+ if (msg.role === "assistant") {
149
+ const content =
150
+ typeof msg.content === "string"
151
+ ? msg.content
152
+ : Array.isArray(msg.content)
153
+ ? msg.content.map((b) => b.text || "").join("\n")
154
+ : "";
155
+
156
+ // Detect if this looks like API endpoint code
157
+ if (containsApiCode(content)) {
158
+ generatedCode = content;
159
+ }
160
+ }
161
+ }
162
+
163
+ // Only return if we found actual API code
164
+ if (!generatedCode || !userId) return null;
165
+
166
+ return {
167
+ userId,
168
+ sessionId,
169
+ generatedCode,
170
+ prompt: userPrompt || "No prompt captured",
171
+ timestamp,
172
+ source: "compliance_api",
173
+ };
174
+ } catch (err) {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Detect if code content contains API endpoint definitions.
181
+ * Covers Express, Flask, FastAPI, Django, Spring, Go, Rails patterns.
182
+ */
183
+ function containsApiCode(content) {
184
+ const apiPatterns = [
185
+ // JavaScript/Node.js (Express, Fastify, Hapi)
186
+ /app\.(get|post|put|patch|delete|use)\s*\(\s*['"`]/i,
187
+ /router\.(get|post|put|patch|delete)\s*\(\s*['"`]/i,
188
+ /fastify\.(get|post|put|patch|delete)\s*\(\s*['"`]/i,
189
+
190
+ // Python (Flask, FastAPI, Django)
191
+ /@app\.route\s*\(\s*['"`]/i,
192
+ /@router\.(get|post|put|patch|delete)\s*\(\s*['"`]/i,
193
+ /path\s*\(\s*['"`][^'"]+['"`]\s*,/i,
194
+ /@app\.(get|post|put|patch|delete)\s*\(\s*['"`]/i,
195
+
196
+ // Go (net/http, gin, echo)
197
+ /http\.HandleFunc\s*\(\s*['"`]/i,
198
+ /r\.(GET|POST|PUT|PATCH|DELETE)\s*\(\s*['"`]/i,
199
+ /e\.(GET|POST|PUT|PATCH|DELETE)\s*\(\s*['"`]/i,
200
+
201
+ // Java/Spring
202
+ /@(Get|Post|Put|Delete|Patch)Mapping\s*\(\s*['"`]/i,
203
+ /@RequestMapping\s*\(/i,
204
+
205
+ // Ruby on Rails
206
+ /resources\s+:/i,
207
+ /get\s+['"`][^'"]+['"`]\s*,\s*to:/i,
208
+
209
+ // General endpoint patterns
210
+ /\/api\//i,
211
+ /endpoint/i,
212
+ ];
213
+
214
+ return apiPatterns.some((pattern) => pattern.test(content));
215
+ }
package/config.js ADDED
@@ -0,0 +1,103 @@
1
+ import dotenv from "dotenv";
2
+ dotenv.config();
3
+
4
+ export const config = {
5
+ anthropic: {
6
+ apiKey: process.env.ANTHROPIC_API_KEY || "",
7
+ complianceApiKey: process.env.ANTHROPIC_COMPLIANCE_API_KEY || "",
8
+ complianceBaseUrl:"https://api.anthropic.com/v1",
9
+ pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS || "60000"),
10
+ model: "claude-sonnet-4-20250514",
11
+ },
12
+
13
+ kickload: {
14
+ apiToken: process.env.KICKLOAD_API_TOKEN || "",
15
+ baseUrl: process.env.KICKLOAD_BASE_URL || "https://kickload.neeyatai.com/api",
16
+ defaultThreads: parseInt(process.env.DEFAULT_THREADS || "5"),
17
+ defaultLoopCount: parseInt(process.env.DEFAULT_LOOPS || "2"),
18
+ defaultRampTime: parseInt(process.env.DEFAULT_RAMP || "2"),
19
+ },
20
+
21
+ targetApiBaseUrl: process.env.TARGET_API_BASE_URL || null,
22
+
23
+ // ngrok.enabled is now advisory only — pipeline auto-enables for localhost
24
+ ngrok: {
25
+ enabled: process.env.NGROK_ENABLED !== "false", // default true
26
+ authToken: process.env.NGROK_AUTHTOKEN || "",
27
+ binDir: ".bin",
28
+ },
29
+
30
+ email: {
31
+ provider: process.env.EMAIL_PROVIDER || "smtp",
32
+ smtp: {
33
+ host: process.env.SMTP_HOST || "smtp.gmail.com",
34
+ port: parseInt(process.env.SMTP_PORT || "465"),
35
+ user: process.env.SMTP_USER || "",
36
+ pass: process.env.SMTP_PASS || "",
37
+ },
38
+ sendgrid: { apiKey: process.env.SENDGRID_API_KEY || "" },
39
+ ses: {
40
+ accessKey: process.env.AWS_ACCESS_KEY_ID || "",
41
+ secretKey: process.env.AWS_SECRET_ACCESS_KEY || "",
42
+ region: process.env.AWS_REGION || "us-east-1",
43
+ },
44
+ fromName: process.env.EMAIL_FROM_NAME || "KickLoad Watcher",
45
+ fromAddress: process.env.EMAIL_FROM_ADDRESS || "",
46
+ },
47
+
48
+ watcher: {
49
+ watchPaths: (process.env.WATCH_PATHS || ".").split(",").map(p => p.trim()),
50
+ extensions: [".js", ".ts", ".py", ".go", ".java", ".rb", ".php", ".cs", ".rs"],
51
+ ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**"],
52
+ },
53
+
54
+ identity: {
55
+ mapFile: process.env.IDENTITY_MAP || "./users.json",
56
+ defaultUserId: process.env.DEFAULT_USER_ID || null,
57
+ defaultDevEmail: process.env.DEFAULT_DEVELOPER_EMAIL || null,
58
+ },
59
+
60
+ triggerMode: process.env.TRIGGER_MODE || "claudecode",
61
+
62
+ github: {
63
+ token: process.env.GITHUB_TOKEN || "",
64
+ webhookPort: parseInt(process.env.GITHUB_WEBHOOK_PORT || "3456"),
65
+ webhookSecret: process.env.GITHUB_WEBHOOK_SECRET || "",
66
+ },
67
+
68
+ logLevel: process.env.LOG_LEVEL || "info",
69
+ };
70
+
71
+ export function validateConfig() {
72
+ const errors = [];
73
+ const warnings = [];
74
+
75
+ if (!config.anthropic.apiKey) errors.push("ANTHROPIC_API_KEY is required");
76
+ if (!config.kickload.apiToken) errors.push("KICKLOAD_API_TOKEN is required");
77
+ if (!config.email.fromAddress) errors.push("EMAIL_FROM_ADDRESS is required");
78
+
79
+ if (config.email.provider === "smtp") {
80
+ if (!config.email.smtp.user) errors.push("SMTP_USER is required for smtp provider");
81
+ if (!config.email.smtp.pass) errors.push("SMTP_PASS is required for smtp provider");
82
+ }
83
+
84
+ if (config.email.provider === "sendgrid" && !config.email.sendgrid.apiKey) {
85
+ errors.push("SENDGRID_API_KEY is required when EMAIL_PROVIDER=sendgrid");
86
+ }
87
+
88
+ // ngrok token is a warning, not a hard error — auto-mode handles it gracefully
89
+ if (!config.ngrok.authToken) {
90
+ warnings.push("NGROK_AUTHTOKEN not set — ngrok tunnel will not authenticate (localhost backends cannot be tested)");
91
+ }
92
+
93
+ if (warnings.length) {
94
+ warnings.forEach(w => console.warn(` ⚠️ ${w}`));
95
+ }
96
+
97
+ if (errors.length) {
98
+ console.error("\n❌ Missing required configuration:");
99
+ errors.forEach(e => console.error(` • ${e}`));
100
+ console.error("\n Fix: delete .env and re-run to go through setup again, or edit .env manually.\n");
101
+ process.exit(1);
102
+ }
103
+ }
@@ -0,0 +1,23 @@
1
+ const COOLDOWN_MS = 60_000;
2
+ const lastSent = new Map();
3
+
4
+ export function isEmailAllowed(toEmail, identifier) {
5
+ const key = `${toEmail}:${identifier}`;
6
+ const last = lastSent.get(key);
7
+ return !last || Date.now() - last >= COOLDOWN_MS;
8
+ }
9
+
10
+ export function getCooldownRemaining(toEmail, identifier) {
11
+ const last = lastSent.get(`${toEmail}:${identifier}`);
12
+ if (!last) return 0;
13
+ const rem = COOLDOWN_MS - (Date.now() - last);
14
+ return rem > 0 ? Math.ceil(rem / 1000) : 0;
15
+ }
16
+
17
+ export function recordEmailSent(toEmail, identifier) {
18
+ const key = `${toEmail}:${identifier}`;
19
+ lastSent.set(key, Date.now());
20
+ for (const [k, ts] of lastSent.entries()) {
21
+ if (Date.now() - ts > COOLDOWN_MS * 2) lastSent.delete(k);
22
+ }
23
+ }