whyusersleave 1.0.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/.next/trace +2 -0
  2. package/index.js +245 -0
  3. package/package.json +14 -0
package/.next/trace ADDED
@@ -0,0 +1,2 @@
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"}]
package/index.js ADDED
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+
5
+ const API_KEY = process.env.WUL_API_KEY;
6
+ const API_BASE = process.env.WUL_API_URL || "https://whyusersleave.vercel.app";
7
+
8
+ if (!API_KEY) {
9
+ console.error("Error: WUL_API_KEY environment variable is required.");
10
+ console.error("Get your API key from https://whyusersleave.vercel.app/dashboard");
11
+ process.exit(1);
12
+ }
13
+
14
+ const server = new McpServer({
15
+ name: "whyusersleave",
16
+ version: "1.0.0",
17
+ });
18
+
19
+ async function apiCall(path, options = {}) {
20
+ const url = `${API_BASE}${path}`;
21
+ const res = await fetch(url, {
22
+ ...options,
23
+ headers: {
24
+ Authorization: `Bearer ${API_KEY}`,
25
+ "Content-Type": "application/json",
26
+ ...options.headers,
27
+ },
28
+ });
29
+ if (!res.ok) {
30
+ const text = await res.text();
31
+ throw new Error(`API error ${res.status}: ${text}`);
32
+ }
33
+ return res;
34
+ }
35
+
36
+ // Tool 1: get_ux_issues
37
+ server.tool(
38
+ "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.",
40
+ {},
41
+ async () => {
42
+ const res = await apiCall("/api/issues");
43
+ const data = await res.json();
44
+ const { issues, summary, lastRun } = data;
45
+
46
+ if (!issues || issues.length === 0) {
47
+ 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
+ }],
52
+ };
53
+ }
54
+
55
+ const lines = [];
56
+
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`);
60
+
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
+ }
69
+
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
+ }
94
+
95
+ // Context
96
+ const context = issue.extra?.context || issue.description;
97
+ if (context) lines.push(` ${context}`);
98
+
99
+ if (issue.selector) lines.push(` Selector: \`${issue.selector}\``);
100
+ lines.push("");
101
+ }
102
+ }
103
+
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"})`);
109
+ }
110
+ if (fixed.length > 5) lines.push(`- ...and ${fixed.length - 5} more`);
111
+ }
112
+
113
+ return {
114
+ content: [{ type: "text", text: lines.join("\n") }],
115
+ };
116
+ }
117
+ );
118
+
119
+ // Tool 2: generate_report
120
+ server.tool(
121
+ "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.",
123
+ {},
124
+ 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();
134
+
135
+ return {
136
+ content: [{ type: "text", text }],
137
+ };
138
+ }
139
+ );
140
+
141
+ // Tool 3: verify_fixes
142
+ server.tool(
143
+ "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.",
145
+ {},
146
+ async () => {
147
+ // Step 1: Re-aggregate fresh data
148
+ await apiCall("/api/insights/aggregate", {
149
+ method: "POST",
150
+ body: JSON.stringify({}),
151
+ });
152
+
153
+ // Step 2: Re-generate export (this updates issue tracking)
154
+ await apiCall("/api/insights/export").then((r) => r.text());
155
+
156
+ // Step 3: Get updated issues with deltas
157
+ const issuesRes = await apiCall("/api/issues");
158
+ const { issues, summary, lastRun } = await issuesRes.json();
159
+
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 ✓`);
173
+ }
174
+ lines.push("");
175
+ }
176
+
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}`);
183
+ }
184
+ lines.push("");
185
+ }
186
+
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}`);
194
+ }
195
+ lines.push("");
196
+ }
197
+
198
+ if (fixed.length === 0 && improved.length === 0 && open.length === 0) {
199
+ lines.push("No issues tracked yet. Run `generate_report` first.");
200
+ }
201
+
202
+ return {
203
+ content: [{ type: "text", text: lines.join("\n") }],
204
+ };
205
+ }
206
+ );
207
+
208
+ // Tool 4: get_site_context
209
+ server.tool(
210
+ "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.",
212
+ {},
213
+ async () => {
214
+ const res = await apiCall("/api/me");
215
+ const { site } = await res.json();
216
+
217
+ 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}`);
230
+ }
231
+ }
232
+
233
+ if (!ctx.description) {
234
+ lines.push("No product context configured yet. Set it up at https://whyusersleave.vercel.app/dashboard");
235
+ }
236
+
237
+ return {
238
+ content: [{ type: "text", text: lines.join("\n") }],
239
+ };
240
+ }
241
+ );
242
+
243
+ // Start server
244
+ const transport = new StdioServerTransport();
245
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "whyusersleave",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for WhyUsersLeave UX analytics — get UX issues, generate reports, and verify fixes directly in Claude Code",
5
+ "bin": {
6
+ "whyusersleave": "./index.js"
7
+ },
8
+ "type": "module",
9
+ "keywords": ["mcp", "claude", "ux", "analytics", "dead-clicks", "user-behavior"],
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "@modelcontextprotocol/sdk": "^1.12.0"
13
+ }
14
+ }