whyusersleave 1.0.0 → 1.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.
Files changed (3) hide show
  1. package/index.js +212 -135
  2. package/package.json +1 -1
  3. package/.next/trace +0 -2
package/index.js CHANGED
@@ -13,9 +13,12 @@ if (!API_KEY) {
13
13
 
14
14
  const server = new McpServer({
15
15
  name: "whyusersleave",
16
- version: "1.0.0",
16
+ version: "1.1.0",
17
17
  });
18
18
 
19
+ // Cache resolved sites
20
+ let resolvedSites = null;
21
+
19
22
  async function apiCall(path, options = {}) {
20
23
  const url = `${API_BASE}${path}`;
21
24
  const res = await fetch(url, {
@@ -33,85 +36,140 @@ async function apiCall(path, options = {}) {
33
36
  return res;
34
37
  }
35
38
 
39
+ /** Fire-and-forget tool call tracking */
40
+ function trackToolCall(toolName) {
41
+ fetch(`${API_BASE}/api/mcp/activity`, {
42
+ method: "POST",
43
+ headers: {
44
+ Authorization: `Bearer ${API_KEY}`,
45
+ "Content-Type": "application/json",
46
+ },
47
+ body: JSON.stringify({ tool_name: toolName }),
48
+ }).catch(() => {});
49
+ }
50
+
51
+ /**
52
+ * Resolve sites from the API key.
53
+ * User keys (wul_u_*) return multiple sites.
54
+ * Site keys return a single site.
55
+ */
56
+ async function getSites() {
57
+ if (resolvedSites) return resolvedSites;
58
+
59
+ const res = await apiCall("/api/me");
60
+ const data = await res.json();
61
+
62
+ if (data.sites) {
63
+ // User-level key — multiple sites
64
+ resolvedSites = data.sites;
65
+ } else if (data.site) {
66
+ // Site-level key — single site
67
+ resolvedSites = [data.site];
68
+ } else {
69
+ resolvedSites = [];
70
+ }
71
+
72
+ return resolvedSites;
73
+ }
74
+
75
+ /** Build site_id query param for API calls */
76
+ function siteParam(siteId) {
77
+ return siteId ? `site_id=${siteId}` : "";
78
+ }
79
+
80
+ /** Join query params */
81
+ function qs(...params) {
82
+ const valid = params.filter(Boolean);
83
+ return valid.length > 0 ? `?${valid.join("&")}` : "";
84
+ }
85
+
36
86
  // Tool 1: get_ux_issues
37
87
  server.tool(
38
88
  "get_ux_issues",
39
- "Get current UX issues detected on your site. Returns open issues with severity, context, and before/after deltas. Use this to check what needs fixing.",
89
+ "Get current UX issues detected on your site(s). Returns open issues with severity, context, and before/after deltas. Use this to check what needs fixing.",
40
90
  {},
41
91
  async () => {
42
- const res = await apiCall("/api/issues");
43
- const data = await res.json();
44
- const { issues, summary, lastRun } = data;
92
+ const sites = await getSites();
93
+ trackToolCall("get_ux_issues");
45
94
 
46
- if (!issues || issues.length === 0) {
95
+ if (sites.length === 0) {
47
96
  return {
48
- content: [{
49
- type: "text",
50
- text: "No UX issues tracked yet. Run `generate_report` first to analyze your site and start tracking issues.",
51
- }],
97
+ content: [{ type: "text", text: "No sites found for this API key." }],
52
98
  };
53
99
  }
54
100
 
55
- const lines = [];
101
+ const allLines = [];
56
102
 
57
- // Summary
58
- lines.push(`## UX Issues Summary`);
59
- lines.push(`${summary.open || 0} open, ${summary.improved || 0} improving, ${summary.fixed || 0} fixed (${summary.totalTracked || 0} total tracked)\n`);
103
+ for (const site of sites) {
104
+ const res = await apiCall(`/api/issues${qs(siteParam(site.id))}`);
105
+ const data = await res.json();
106
+ const { issues, summary, lastRun } = data;
60
107
 
61
- // Last run info
62
- if (lastRun && lastRun.sentCount > 0) {
63
- lines.push(`**Last report:** Sent ${lastRun.sentCount} issues to Claude Code`);
64
- if (lastRun.fixedCount > 0) lines.push(` - ${lastRun.fixedCount} fixed`);
65
- if (lastRun.improvedCount > 0) lines.push(` - ${lastRun.improvedCount} improving`);
66
- if (lastRun.stillOpenCount > 0) lines.push(` - ${lastRun.stillOpenCount} still open`);
67
- lines.push("");
68
- }
108
+ if (sites.length > 1) {
109
+ allLines.push(`# ${site.name || site.domain}\n`);
110
+ }
69
111
 
70
- // Group by severity
71
- const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
72
- const open = issues
73
- .filter((i) => i.status === "open" || i.status === "regressed" || i.status === "improved")
74
- .sort((a, b) => (severityOrder[a.severity] ?? 2) - (severityOrder[b.severity] ?? 2));
75
-
76
- const fixed = issues.filter((i) => i.status === "fixed");
77
-
78
- if (open.length > 0) {
79
- lines.push("### Open Issues\n");
80
- for (const issue of open) {
81
- const sev = (issue.severity || "medium").toUpperCase();
82
- const status = issue.status === "improved" ? " (improving)" : issue.status === "regressed" ? " (REGRESSED)" : "";
83
- lines.push(`**[${sev}]** ${issue.title}${status}`);
84
- lines.push(` Page: ${issue.page}`);
85
-
86
- // Show delta if available
87
- if (issue.baseline_value !== undefined && issue.baseline_value !== null) {
88
- const delta = issue.delta || 0;
89
- const direction = delta < 0 ? "improving" : delta > 0 ? "worsening" : "no change";
90
- lines.push(` Value: ${Math.round(issue.baseline_value)} → ${Math.round(issue.current_value)} (${delta > 0 ? "+" : ""}${Math.round(delta)}, ${direction})`);
91
- } else {
92
- lines.push(` Value: ${Math.round(issue.current_value)}`);
93
- }
112
+ if (!issues || issues.length === 0) {
113
+ allLines.push("No UX issues tracked yet. Run `generate_report` first to analyze your site and start tracking issues.\n");
114
+ continue;
115
+ }
94
116
 
95
- // Context
96
- const context = issue.extra?.context || issue.description;
97
- if (context) lines.push(` ${context}`);
117
+ // Summary
118
+ allLines.push(`## UX Issues Summary`);
119
+ allLines.push(`${summary.open || 0} open, ${summary.improved || 0} improving, ${summary.fixed || 0} fixed (${summary.totalTracked || 0} total tracked)\n`);
120
+
121
+ // Last run info
122
+ if (lastRun && lastRun.sentCount > 0) {
123
+ allLines.push(`**Last report:** Sent ${lastRun.sentCount} issues to Claude Code`);
124
+ if (lastRun.fixedCount > 0) allLines.push(` - ${lastRun.fixedCount} fixed`);
125
+ if (lastRun.improvedCount > 0) allLines.push(` - ${lastRun.improvedCount} improving`);
126
+ if (lastRun.stillOpenCount > 0) allLines.push(` - ${lastRun.stillOpenCount} still open`);
127
+ allLines.push("");
128
+ }
98
129
 
99
- if (issue.selector) lines.push(` Selector: \`${issue.selector}\``);
100
- lines.push("");
130
+ // Group by severity
131
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
132
+ const open = issues
133
+ .filter((i) => i.status === "open" || i.status === "regressed" || i.status === "improved")
134
+ .sort((a, b) => (severityOrder[a.severity] ?? 2) - (severityOrder[b.severity] ?? 2));
135
+
136
+ const fixed = issues.filter((i) => i.status === "fixed");
137
+
138
+ if (open.length > 0) {
139
+ allLines.push("### Open Issues\n");
140
+ for (const issue of open) {
141
+ const sev = (issue.severity || "medium").toUpperCase();
142
+ const status = issue.status === "improved" ? " (improving)" : issue.status === "regressed" ? " (REGRESSED)" : "";
143
+ allLines.push(`**[${sev}]** ${issue.title}${status}`);
144
+ allLines.push(` Page: ${issue.page}`);
145
+
146
+ if (issue.baseline_value !== undefined && issue.baseline_value !== null) {
147
+ const delta = issue.delta || 0;
148
+ const direction = delta < 0 ? "improving" : delta > 0 ? "worsening" : "no change";
149
+ allLines.push(` Value: ${Math.round(issue.baseline_value)} → ${Math.round(issue.current_value)} (${delta > 0 ? "+" : ""}${Math.round(delta)}, ${direction})`);
150
+ } else {
151
+ allLines.push(` Value: ${Math.round(issue.current_value)}`);
152
+ }
153
+
154
+ const context = issue.extra?.context || issue.description;
155
+ if (context) allLines.push(` ${context}`);
156
+ if (issue.selector) allLines.push(` Selector: \`${issue.selector}\``);
157
+ allLines.push("");
158
+ }
101
159
  }
102
- }
103
160
 
104
- if (fixed.length > 0) {
105
- lines.push(`### Fixed (${fixed.length})\n`);
106
- for (const issue of fixed.slice(0, 5)) {
107
- const baseVal = issue.baseline_value !== undefined ? Math.round(issue.baseline_value) : Math.round(issue.peak_value || 0);
108
- lines.push(`- ~~${issue.title}~~ (${baseVal} → 0, fixed ${issue.fixed_at ? new Date(issue.fixed_at).toLocaleDateString() : "recently"})`);
161
+ if (fixed.length > 0) {
162
+ allLines.push(`### Fixed (${fixed.length})\n`);
163
+ for (const issue of fixed.slice(0, 5)) {
164
+ const baseVal = issue.baseline_value !== undefined ? Math.round(issue.baseline_value) : Math.round(issue.peak_value || 0);
165
+ allLines.push(`- ~~${issue.title}~~ (${baseVal} → 0, fixed ${issue.fixed_at ? new Date(issue.fixed_at).toLocaleDateString() : "recently"})`);
166
+ }
167
+ if (fixed.length > 5) allLines.push(`- ...and ${fixed.length - 5} more`);
109
168
  }
110
- if (fixed.length > 5) lines.push(`- ...and ${fixed.length - 5} more`);
111
169
  }
112
170
 
113
171
  return {
114
- content: [{ type: "text", text: lines.join("\n") }],
172
+ content: [{ type: "text", text: allLines.join("\n") }],
115
173
  };
116
174
  }
117
175
  );
@@ -119,21 +177,30 @@ server.tool(
119
177
  // Tool 2: generate_report
120
178
  server.tool(
121
179
  "generate_report",
122
- "Generate a comprehensive UX report with data tables and AI analysis. This runs the full aggregate pipeline on recent session data, then produces a 10-section report including dead clicks, JS errors, form abandonment, page performance, session examples, and a strategic AI analysis. Takes 15-30 seconds.",
180
+ "Generate a comprehensive UX report with data tables and AI analysis. This runs the full aggregate pipeline on recent session data, then produces a 10-section report. Takes 15-30 seconds. If you have multiple sites, generates a report for each.",
123
181
  {},
124
182
  async () => {
125
- // Step 1: Run aggregate pipeline
126
- await apiCall("/api/insights/aggregate", {
127
- method: "POST",
128
- body: JSON.stringify({}),
129
- });
130
-
131
- // Step 2: Stream the export
132
- const res = await apiCall("/api/insights/export");
133
- const text = await res.text();
183
+ const sites = await getSites();
184
+ trackToolCall("generate_report");
185
+ const allOutput = [];
186
+
187
+ for (const site of sites) {
188
+ if (sites.length > 1) {
189
+ allOutput.push(`# ${site.name || site.domain}\n`);
190
+ }
191
+
192
+ await apiCall(`/api/insights/aggregate${qs(siteParam(site.id))}`, {
193
+ method: "POST",
194
+ body: JSON.stringify({}),
195
+ });
196
+
197
+ const res = await apiCall(`/api/insights/export${qs(siteParam(site.id))}`);
198
+ const text = await res.text();
199
+ allOutput.push(text);
200
+ }
134
201
 
135
202
  return {
136
- content: [{ type: "text", text }],
203
+ content: [{ type: "text", text: allOutput.join("\n\n---\n\n") }],
137
204
  };
138
205
  }
139
206
  );
@@ -141,66 +208,72 @@ server.tool(
141
208
  // Tool 3: verify_fixes
142
209
  server.tool(
143
210
  "verify_fixes",
144
- "Re-analyze your site with fresh session data to see if your fixes worked. Runs the aggregate pipeline on recent sessions (last 24 hours preferred), then returns a before/after comparison of all tracked issues. Use this after deploying fixes to confirm they worked.",
211
+ "Re-analyze your site(s) with fresh session data to see if your fixes worked. Returns a before/after comparison of all tracked issues.",
145
212
  {},
146
213
  async () => {
147
- // Step 1: Re-aggregate fresh data
148
- await apiCall("/api/insights/aggregate", {
149
- method: "POST",
150
- body: JSON.stringify({}),
151
- });
214
+ const sites = await getSites();
215
+ trackToolCall("verify_fixes");
216
+ const allLines = [];
217
+
218
+ for (const site of sites) {
219
+ if (sites.length > 1) {
220
+ allLines.push(`# ${site.name || site.domain}\n`);
221
+ }
152
222
 
153
- // Step 2: Re-generate export (this updates issue tracking)
154
- await apiCall("/api/insights/export").then((r) => r.text());
223
+ await apiCall(`/api/insights/aggregate${qs(siteParam(site.id))}`, {
224
+ method: "POST",
225
+ body: JSON.stringify({}),
226
+ });
155
227
 
156
- // Step 3: Get updated issues with deltas
157
- const issuesRes = await apiCall("/api/issues");
158
- const { issues, summary, lastRun } = await issuesRes.json();
228
+ await apiCall(`/api/insights/export${qs(siteParam(site.id))}`).then((r) => r.text());
159
229
 
160
- const lines = [];
161
- lines.push("## Verification Results\n");
162
- lines.push(`Re-aggregated fresh session data. ${summary.totalTracked || 0} issues tracked.\n`);
163
-
164
- const fixed = issues.filter((i) => i.status === "fixed");
165
- const improved = issues.filter((i) => i.status === "improved");
166
- const open = issues.filter((i) => i.status === "open" || i.status === "regressed");
167
-
168
- if (fixed.length > 0) {
169
- lines.push(`### Fixed (${fixed.length})\n`);
170
- for (const issue of fixed) {
171
- const baseVal = issue.baseline_value !== undefined ? Math.round(issue.baseline_value) : Math.round(issue.peak_value || 0);
172
- lines.push(`- ${issue.title}: ${baseVal} 0 ✓`);
230
+ const issuesRes = await apiCall(`/api/issues${qs(siteParam(site.id))}`);
231
+ const { issues, summary } = await issuesRes.json();
232
+
233
+ allLines.push("## Verification Results\n");
234
+ allLines.push(`Re-aggregated fresh session data. ${summary.totalTracked || 0} issues tracked.\n`);
235
+
236
+ const fixed = issues.filter((i) => i.status === "fixed");
237
+ const improved = issues.filter((i) => i.status === "improved");
238
+ const open = issues.filter((i) => i.status === "open" || i.status === "regressed");
239
+
240
+ if (fixed.length > 0) {
241
+ allLines.push(`### Fixed (${fixed.length})\n`);
242
+ for (const issue of fixed) {
243
+ const baseVal = issue.baseline_value !== undefined ? Math.round(issue.baseline_value) : Math.round(issue.peak_value || 0);
244
+ allLines.push(`- ${issue.title}: ${baseVal} → 0 ✓`);
245
+ }
246
+ allLines.push("");
173
247
  }
174
- lines.push("");
175
- }
176
248
 
177
- if (improved.length > 0) {
178
- lines.push(`### Improving (${improved.length})\n`);
179
- for (const issue of improved) {
180
- const baseVal = issue.baseline_value !== undefined ? Math.round(issue.baseline_value) : "?";
181
- const delta = issue.delta !== undefined ? ` (${issue.delta > 0 ? "+" : ""}${Math.round(issue.delta)})` : "";
182
- lines.push(`- ${issue.title}: ${baseVal} → ${Math.round(issue.current_value)}${delta}`);
249
+ if (improved.length > 0) {
250
+ allLines.push(`### Improving (${improved.length})\n`);
251
+ for (const issue of improved) {
252
+ const baseVal = issue.baseline_value !== undefined ? Math.round(issue.baseline_value) : "?";
253
+ const delta = issue.delta !== undefined ? ` (${issue.delta > 0 ? "+" : ""}${Math.round(issue.delta)})` : "";
254
+ allLines.push(`- ${issue.title}: ${baseVal} → ${Math.round(issue.current_value)}${delta}`);
255
+ }
256
+ allLines.push("");
183
257
  }
184
- lines.push("");
185
- }
186
258
 
187
- if (open.length > 0) {
188
- lines.push(`### Still Open (${open.length})\n`);
189
- for (const issue of open) {
190
- const baseVal = issue.baseline_value !== undefined ? Math.round(issue.baseline_value) : "?";
191
- const delta = issue.delta !== undefined ? ` (${issue.delta > 0 ? "+" : ""}${Math.round(issue.delta)})` : "";
192
- const sev = (issue.severity || "medium").toUpperCase();
193
- lines.push(`- [${sev}] ${issue.title}: ${baseVal} → ${Math.round(issue.current_value)}${delta}`);
259
+ if (open.length > 0) {
260
+ allLines.push(`### Still Open (${open.length})\n`);
261
+ for (const issue of open) {
262
+ const baseVal = issue.baseline_value !== undefined ? Math.round(issue.baseline_value) : "?";
263
+ const delta = issue.delta !== undefined ? ` (${issue.delta > 0 ? "+" : ""}${Math.round(issue.delta)})` : "";
264
+ const sev = (issue.severity || "medium").toUpperCase();
265
+ allLines.push(`- [${sev}] ${issue.title}: ${baseVal} → ${Math.round(issue.current_value)}${delta}`);
266
+ }
267
+ allLines.push("");
194
268
  }
195
- lines.push("");
196
- }
197
269
 
198
- if (fixed.length === 0 && improved.length === 0 && open.length === 0) {
199
- lines.push("No issues tracked yet. Run `generate_report` first.");
270
+ if (fixed.length === 0 && improved.length === 0 && open.length === 0) {
271
+ allLines.push("No issues tracked yet. Run `generate_report` first.\n");
272
+ }
200
273
  }
201
274
 
202
275
  return {
203
- content: [{ type: "text", text: lines.join("\n") }],
276
+ content: [{ type: "text", text: allLines.join("\n") }],
204
277
  };
205
278
  }
206
279
  );
@@ -208,30 +281,34 @@ server.tool(
208
281
  // Tool 4: get_site_context
209
282
  server.tool(
210
283
  "get_site_context",
211
- "Get product context for your site — description, key pages, user journey, business model. Useful for understanding the app you're working on.",
284
+ "Get product context for your site(s) — description, key pages, user journey, business model.",
212
285
  {},
213
286
  async () => {
214
- const res = await apiCall("/api/me");
215
- const { site } = await res.json();
216
-
287
+ const sites = await getSites();
288
+ trackToolCall("get_site_context");
217
289
  const lines = [];
218
- lines.push(`## ${site.name || "Site"} (${site.domain || "unknown domain"})\n`);
219
-
220
- const ctx = site.context || {};
221
- if (ctx.description) lines.push(ctx.description);
222
- if (ctx.user_journey) lines.push(`\n**User journey:** ${ctx.user_journey}`);
223
- if (ctx.business_model) lines.push(`**Business model:** ${ctx.business_model}`);
224
- if (ctx.success_metrics) lines.push(`**Success metrics:** ${ctx.success_metrics}`);
225
-
226
- if (ctx.key_pages && typeof ctx.key_pages === "object") {
227
- lines.push("\n**Key pages:**");
228
- for (const [path, desc] of Object.entries(ctx.key_pages)) {
229
- lines.push(`- ${path}: ${desc}`);
290
+
291
+ for (const site of sites) {
292
+ lines.push(`## ${site.name || "Site"} (${site.domain || "unknown domain"})\n`);
293
+
294
+ const ctx = site.context || {};
295
+ if (ctx.description) lines.push(ctx.description);
296
+ if (ctx.user_journey) lines.push(`\n**User journey:** ${ctx.user_journey}`);
297
+ if (ctx.business_model) lines.push(`**Business model:** ${ctx.business_model}`);
298
+ if (ctx.success_metrics) lines.push(`**Success metrics:** ${ctx.success_metrics}`);
299
+
300
+ if (ctx.key_pages && typeof ctx.key_pages === "object") {
301
+ lines.push("\n**Key pages:**");
302
+ for (const [path, desc] of Object.entries(ctx.key_pages)) {
303
+ lines.push(`- ${path}: ${desc}`);
304
+ }
230
305
  }
231
- }
232
306
 
233
- if (!ctx.description) {
234
- lines.push("No product context configured yet. Set it up at https://whyusersleave.vercel.app/dashboard");
307
+ if (!ctx.description) {
308
+ lines.push("No product context configured yet. Set it up at https://whyusersleave.vercel.app/dashboard");
309
+ }
310
+
311
+ lines.push("");
235
312
  }
236
313
 
237
314
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whyusersleave",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for WhyUsersLeave UX analytics — get UX issues, generate reports, and verify fixes directly in Claude Code",
5
5
  "bin": {
6
6
  "whyusersleave": "./index.js"
package/.next/trace DELETED
@@ -1,2 +0,0 @@
1
- [{"name":"generate-buildid","duration":284,"timestamp":700550080762,"id":4,"parentId":1,"tags":{},"startTime":1770965442106,"traceId":"291b02670db969a6"},{"name":"load-custom-routes","duration":233,"timestamp":700550081483,"id":5,"parentId":1,"tags":{},"startTime":1770965442106,"traceId":"291b02670db969a6"},{"name":"next-build","duration":95028,"timestamp":700549989812,"id":1,"tags":{"buildMode":"default","isTurboBuild":"false","version":"14.2.21","isTurbopack":false},"startTime":1770965442015,"traceId":"291b02670db969a6"}]
2
- [{"name":"generate-buildid","duration":280,"timestamp":700557038698,"id":4,"parentId":1,"tags":{},"startTime":1770965449063,"traceId":"7da191ad32ede36c"},{"name":"load-custom-routes","duration":207,"timestamp":700557039344,"id":5,"parentId":1,"tags":{},"startTime":1770965449064,"traceId":"7da191ad32ede36c"},{"name":"next-build","duration":90123,"timestamp":700556951361,"id":1,"tags":{"buildMode":"default","isTurboBuild":"false","version":"14.2.21","isTurbopack":false},"startTime":1770965448976,"traceId":"7da191ad32ede36c"}]