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.
- package/.next/trace +2 -0
- package/index.js +245 -0
- 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
|
+
}
|