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.
- package/index.js +212 -135
- package/package.json +1 -1
- 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.
|
|
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
|
|
43
|
-
|
|
44
|
-
const { issues, summary, lastRun } = data;
|
|
92
|
+
const sites = await getSites();
|
|
93
|
+
trackToolCall("get_ux_issues");
|
|
45
94
|
|
|
46
|
-
if (
|
|
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
|
|
101
|
+
const allLines = [];
|
|
56
102
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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.
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
223
|
+
await apiCall(`/api/insights/aggregate${qs(siteParam(site.id))}`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
body: JSON.stringify({}),
|
|
226
|
+
});
|
|
155
227
|
|
|
156
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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:
|
|
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.
|
|
284
|
+
"Get product context for your site(s) — description, key pages, user journey, business model.",
|
|
212
285
|
{},
|
|
213
286
|
async () => {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
287
|
+
const sites = await getSites();
|
|
288
|
+
trackToolCall("get_site_context");
|
|
217
289
|
const lines = [];
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
lines.push(
|
|
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
|
-
|
|
234
|
-
|
|
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
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"}]
|