@recapt/mcp 0.0.2-beta
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/dist/api/client.d.ts +12 -0
- package/dist/api/client.js +67 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +217 -0
- package/dist/tools/analyzeFlow.d.ts +7 -0
- package/dist/tools/analyzeFlow.js +68 -0
- package/dist/tools/analyzeFunnel.d.ts +7 -0
- package/dist/tools/analyzeFunnel.js +63 -0
- package/dist/tools/compareCohorts.d.ts +6 -0
- package/dist/tools/compareCohorts.js +84 -0
- package/dist/tools/comparePeriods.d.ts +6 -0
- package/dist/tools/comparePeriods.js +54 -0
- package/dist/tools/detectDrift.d.ts +6 -0
- package/dist/tools/detectDrift.js +55 -0
- package/dist/tools/detectRegressions.d.ts +6 -0
- package/dist/tools/detectRegressions.js +46 -0
- package/dist/tools/discoverPersonas.d.ts +6 -0
- package/dist/tools/discoverPersonas.js +50 -0
- package/dist/tools/getActionableIssues.d.ts +7 -0
- package/dist/tools/getActionableIssues.js +55 -0
- package/dist/tools/getAnomalies.d.ts +6 -0
- package/dist/tools/getAnomalies.js +53 -0
- package/dist/tools/getConsoleErrors.d.ts +6 -0
- package/dist/tools/getConsoleErrors.js +61 -0
- package/dist/tools/getDeadClicks.d.ts +6 -0
- package/dist/tools/getDeadClicks.js +42 -0
- package/dist/tools/getDomains.d.ts +6 -0
- package/dist/tools/getDomains.js +34 -0
- package/dist/tools/getElementFriction.d.ts +6 -0
- package/dist/tools/getElementFriction.js +45 -0
- package/dist/tools/getFlowFriction.d.ts +7 -0
- package/dist/tools/getFlowFriction.js +57 -0
- package/dist/tools/getFormFriction.d.ts +6 -0
- package/dist/tools/getFormFriction.js +42 -0
- package/dist/tools/getIssues.d.ts +6 -0
- package/dist/tools/getIssues.js +82 -0
- package/dist/tools/getJourneyPatterns.d.ts +7 -0
- package/dist/tools/getJourneyPatterns.js +50 -0
- package/dist/tools/getPageMetrics.d.ts +6 -0
- package/dist/tools/getPageMetrics.js +47 -0
- package/dist/tools/getPageTrends.d.ts +6 -0
- package/dist/tools/getPageTrends.js +46 -0
- package/dist/tools/getSessionDetails.d.ts +6 -0
- package/dist/tools/getSessionDetails.js +70 -0
- package/dist/tools/getUxHealthReport.d.ts +7 -0
- package/dist/tools/getUxHealthReport.js +50 -0
- package/dist/tools/listPages.d.ts +6 -0
- package/dist/tools/listPages.js +50 -0
- package/dist/tools/listSessions.d.ts +7 -0
- package/dist/tools/listSessions.js +67 -0
- package/dist/tools/memory.d.ts +7 -0
- package/dist/tools/memory.js +119 -0
- package/dist/tools/predictOutcomes.d.ts +6 -0
- package/dist/tools/predictOutcomes.js +66 -0
- package/dist/tools/scanSite.d.ts +6 -0
- package/dist/tools/scanSite.js +51 -0
- package/dist/tools/searchSessions.d.ts +6 -0
- package/dist/tools/searchSessions.js +51 -0
- package/dist/tools/triageSessions.d.ts +8 -0
- package/dist/tools/triageSessions.js +195 -0
- package/dist/utils/orgContext.d.ts +6 -0
- package/dist/utils/orgContext.js +22 -0
- package/package.json +37 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for calling external-api query endpoints.
|
|
3
|
+
*/
|
|
4
|
+
export declare function isApiConfigured(): boolean;
|
|
5
|
+
export declare function getApiUrl(): string;
|
|
6
|
+
interface ApiResponse<T> {
|
|
7
|
+
data?: T;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function apiGet<T>(path: string, params?: Record<string, string | number | undefined>): Promise<ApiResponse<T>>;
|
|
11
|
+
export declare function apiPost<T>(path: string, body: Record<string, unknown>): Promise<ApiResponse<T>>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for calling external-api query endpoints.
|
|
3
|
+
*/
|
|
4
|
+
const API_URL = process.env.EXTERNAL_API_URL || "http://localhost:4000";
|
|
5
|
+
const SECRET_KEY = process.env.RECAPT_SECRET_KEY;
|
|
6
|
+
export function isApiConfigured() {
|
|
7
|
+
return !!SECRET_KEY;
|
|
8
|
+
}
|
|
9
|
+
export function getApiUrl() {
|
|
10
|
+
return API_URL;
|
|
11
|
+
}
|
|
12
|
+
export async function apiGet(path, params) {
|
|
13
|
+
if (!SECRET_KEY) {
|
|
14
|
+
return { error: "RECAPT_SECRET_KEY not configured" };
|
|
15
|
+
}
|
|
16
|
+
const url = new URL(`${API_URL}/v1beta/query${path}`);
|
|
17
|
+
if (params) {
|
|
18
|
+
for (const [key, value] of Object.entries(params)) {
|
|
19
|
+
if (value !== undefined) {
|
|
20
|
+
url.searchParams.set(key, String(value));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(url.toString(), {
|
|
26
|
+
method: "GET",
|
|
27
|
+
headers: {
|
|
28
|
+
"x-private-key": SECRET_KEY,
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const body = (await res.json().catch(() => ({})));
|
|
34
|
+
return { error: body.error || `HTTP ${res.status}` };
|
|
35
|
+
}
|
|
36
|
+
const data = (await res.json());
|
|
37
|
+
return { data };
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function apiPost(path, body) {
|
|
44
|
+
if (!SECRET_KEY) {
|
|
45
|
+
return { error: "RECAPT_SECRET_KEY not configured" };
|
|
46
|
+
}
|
|
47
|
+
const url = `${API_URL}/v1beta/query${path}`;
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(url, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: {
|
|
52
|
+
"x-private-key": SECRET_KEY,
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify(body),
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const responseData = (await res.json().catch(() => ({})));
|
|
59
|
+
return { error: responseData.error || `HTTP ${res.status}` };
|
|
60
|
+
}
|
|
61
|
+
const data = (await res.json());
|
|
62
|
+
return { data };
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP Server entry point.
|
|
4
|
+
*
|
|
5
|
+
* Exposes recapt behavioral intelligence as tools for AI coding agents.
|
|
6
|
+
* Acts as a thin proxy to external-api query endpoints.
|
|
7
|
+
*/
|
|
8
|
+
import "dotenv/config";
|
|
9
|
+
// @ts-ignore - MCP SDK types are complex
|
|
10
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
+
// @ts-ignore - MCP SDK types are complex
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { isApiConfigured, getApiUrl } from "./api/client.js";
|
|
14
|
+
import { registerGetPageMetrics } from "./tools/getPageMetrics.js";
|
|
15
|
+
import { registerGetElementFriction } from "./tools/getElementFriction.js";
|
|
16
|
+
import { registerSearchSessions } from "./tools/searchSessions.js";
|
|
17
|
+
import { registerGetIssues } from "./tools/getIssues.js";
|
|
18
|
+
import { registerGetSessionDetails } from "./tools/getSessionDetails.js";
|
|
19
|
+
import { registerGetPageTrends } from "./tools/getPageTrends.js";
|
|
20
|
+
import { registerGetAnomalies } from "./tools/getAnomalies.js";
|
|
21
|
+
import { registerGetDomains } from "./tools/getDomains.js";
|
|
22
|
+
import { registerGetActionableIssues } from "./tools/getActionableIssues.js";
|
|
23
|
+
import { registerGetUxHealthReport } from "./tools/getUxHealthReport.js";
|
|
24
|
+
import { registerAnalyzeFlow } from "./tools/analyzeFlow.js";
|
|
25
|
+
import { registerAnalyzeFunnel } from "./tools/analyzeFunnel.js";
|
|
26
|
+
import { registerGetFlowFriction } from "./tools/getFlowFriction.js";
|
|
27
|
+
import { registerGetJourneyPatterns } from "./tools/getJourneyPatterns.js";
|
|
28
|
+
import { registerGetDeadClicks } from "./tools/getDeadClicks.js";
|
|
29
|
+
import { registerGetConsoleErrors } from "./tools/getConsoleErrors.js";
|
|
30
|
+
import { registerCompareCohorts } from "./tools/compareCohorts.js";
|
|
31
|
+
import { registerDetectRegressions } from "./tools/detectRegressions.js";
|
|
32
|
+
import { registerDiscoverPersonas } from "./tools/discoverPersonas.js";
|
|
33
|
+
import { registerGetFormFriction } from "./tools/getFormFriction.js";
|
|
34
|
+
import { registerScanSite } from "./tools/scanSite.js";
|
|
35
|
+
import { registerListPages } from "./tools/listPages.js";
|
|
36
|
+
import { registerComparePeriods } from "./tools/comparePeriods.js";
|
|
37
|
+
import { registerDetectDrift } from "./tools/detectDrift.js";
|
|
38
|
+
import { registerPredictOutcomes } from "./tools/predictOutcomes.js";
|
|
39
|
+
import { registerMemoryTools } from "./tools/memory.js";
|
|
40
|
+
import { registerListSessions } from "./tools/listSessions.js";
|
|
41
|
+
import { registerTriageSessions } from "./tools/triageSessions.js";
|
|
42
|
+
async function main() {
|
|
43
|
+
console.error("[MCP] Starting recapt behavioral intelligence server...");
|
|
44
|
+
if (!isApiConfigured()) {
|
|
45
|
+
console.error("[MCP] ERROR: RECAPT_SECRET_KEY environment variable is required");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
console.error(`[MCP] API URL: ${getApiUrl()}`);
|
|
49
|
+
const server = new McpServer({
|
|
50
|
+
name: "recapt",
|
|
51
|
+
version: "1.0.0",
|
|
52
|
+
}, {
|
|
53
|
+
capabilities: { logging: {} },
|
|
54
|
+
instructions: `# Recapt Behavioral Intelligence
|
|
55
|
+
|
|
56
|
+
This server provides behavioral data from session recordings. Use these tools to understand user behavior, identify UX issues, and find friction points.
|
|
57
|
+
|
|
58
|
+
## Metric Interpretation
|
|
59
|
+
|
|
60
|
+
All behavioral scores are 0-1 where higher = more of that signal:
|
|
61
|
+
- **frustration** > 0.3: Notable friction, > 0.5: High priority issue
|
|
62
|
+
- **confusion** > 0.3: Users uncertain, > 0.5: Significant clarity problem
|
|
63
|
+
- **confidence** < 0.4: Users hesitant, < 0.25: Major decision friction
|
|
64
|
+
- **health_score**: 0-100 where 85+: A (excellent), 70-84: B (good), 55-69: C (needs work), 40-54: D (poor), <40: F (critical)
|
|
65
|
+
- **drop_off_rate** > 0.3: Major leak point in a flow
|
|
66
|
+
- **spike_ratio** > 2: Critical frustration spike, likely recent regression
|
|
67
|
+
|
|
68
|
+
## Tool Orchestration Strategies
|
|
69
|
+
|
|
70
|
+
### Starting point - understand the landscape
|
|
71
|
+
1. Use \`scan_site\` or \`list_pages\` to see all tracked pages with health grades
|
|
72
|
+
2. Use \`get_ux_health_report\` for a comprehensive overview combining metrics, issues, and anomalies
|
|
73
|
+
3. Use \`get_domains\` to see which domains are being tracked
|
|
74
|
+
|
|
75
|
+
### Diagnosing a problematic page
|
|
76
|
+
1. Start with \`get_page_metrics\` to see overall behavioral scores
|
|
77
|
+
2. If frustration high → \`get_element_friction\` to find specific problematic elements
|
|
78
|
+
3. If confusion high → \`get_dead_clicks\` to find misleading UI elements users click expecting action
|
|
79
|
+
4. Check \`get_console_errors\` for JS errors that may cause unresponsiveness
|
|
80
|
+
5. Use \`get_form_friction\` if the page has forms to find problematic fields
|
|
81
|
+
|
|
82
|
+
### Understanding user flows
|
|
83
|
+
1. \`get_journey_patterns\` for organic navigation discovery (where do users go?)
|
|
84
|
+
2. \`analyze_flow\` between specific pages to see paths, bottlenecks, and friction
|
|
85
|
+
3. \`analyze_funnel\` for conversion analysis through defined steps (cart → checkout → payment)
|
|
86
|
+
4. \`get_flow_friction\` to automatically discover high-friction flows
|
|
87
|
+
|
|
88
|
+
### Finding issues to fix
|
|
89
|
+
1. \`get_issues\` for auto-detected problems (rage clicks, dead clicks, errors)
|
|
90
|
+
2. \`get_actionable_issues\` for issues with element context
|
|
91
|
+
3. \`detect_regressions\` to find recent degradations (compare last 24h vs baseline)
|
|
92
|
+
4. \`get_anomalies\` for unusual sessions and frustration spikes
|
|
93
|
+
|
|
94
|
+
### Comparing segments
|
|
95
|
+
1. \`compare_cohorts\` to understand differences (mobile vs desktop, completed vs abandoned)
|
|
96
|
+
2. \`compare_periods\` to measure impact of changes (before vs after deployment)
|
|
97
|
+
3. \`discover_personas\` to find behavioral user segments
|
|
98
|
+
|
|
99
|
+
### Deep-diving a session
|
|
100
|
+
1. \`search_sessions\` with natural language to find relevant sessions
|
|
101
|
+
2. \`list_sessions\` to browse by domain, status, or device
|
|
102
|
+
3. \`get_session_details\` for behavioral timeline of a specific session
|
|
103
|
+
|
|
104
|
+
### Monitoring over time
|
|
105
|
+
1. \`get_page_trends\` for daily behavioral trends on a page
|
|
106
|
+
2. \`detect_drift\` for gradual behavioral changes over weeks
|
|
107
|
+
3. \`detect_regressions\` for sudden changes (deployment impact)
|
|
108
|
+
|
|
109
|
+
## Reasoning Tips
|
|
110
|
+
|
|
111
|
+
- High rage clicks on body/root elements often indicate JS errors preventing interaction - check \`get_console_errors\`
|
|
112
|
+
- Backtrack hotspots (pages users return to repeatedly) suggest confusion or errors on subsequent pages
|
|
113
|
+
- Drop-off pages may be intentional exits (thank you page) or problems - check if it's a terminal page
|
|
114
|
+
- Compare frustration across device types with \`compare_cohorts\` - mobile often has different issues
|
|
115
|
+
- When frustration spikes recently, use \`detect_regressions\` to correlate with deployments
|
|
116
|
+
- Low confidence + high confusion = users don't understand what to do next
|
|
117
|
+
- High frustration + low confusion = users know what to do but can't (broken UI, errors)
|
|
118
|
+
- Use \`memory_save\` to store intermediate results when doing multi-step analysis
|
|
119
|
+
|
|
120
|
+
## Available Tools
|
|
121
|
+
|
|
122
|
+
### Site Overview
|
|
123
|
+
- \`get_domains\`: List configured domains
|
|
124
|
+
- \`scan_site\`: Quick health scan of top pages
|
|
125
|
+
- \`list_pages\`: List all tracked pages with metrics
|
|
126
|
+
- \`get_ux_health_report\`: Comprehensive health report
|
|
127
|
+
|
|
128
|
+
### Page Analysis
|
|
129
|
+
- \`get_page_metrics\`: Behavioral metrics for a page
|
|
130
|
+
- \`get_element_friction\`: Per-element friction data
|
|
131
|
+
- \`get_page_trends\`: Daily trends over time
|
|
132
|
+
- \`get_dead_clicks\`: Unresponsive elements users click
|
|
133
|
+
- \`get_console_errors\`: JavaScript errors
|
|
134
|
+
- \`get_form_friction\`: Form field friction analysis
|
|
135
|
+
|
|
136
|
+
### Flow Analysis
|
|
137
|
+
- \`analyze_flow\`: Navigation between specific pages
|
|
138
|
+
- \`analyze_funnel\`: Conversion through page sequence
|
|
139
|
+
- \`get_flow_friction\`: Discover high-friction flows
|
|
140
|
+
- \`get_journey_patterns\`: Navigation patterns and hotspots
|
|
141
|
+
|
|
142
|
+
### Issue Detection
|
|
143
|
+
- \`get_issues\`: Auto-detected UX issues
|
|
144
|
+
- \`get_actionable_issues\`: Issues with element context
|
|
145
|
+
- \`get_anomalies\`: Unusual sessions and spikes
|
|
146
|
+
- \`detect_regressions\`: Recent degradations
|
|
147
|
+
|
|
148
|
+
### Comparison & Segmentation
|
|
149
|
+
- \`compare_cohorts\`: Compare user segments
|
|
150
|
+
- \`compare_periods\`: Compare time periods
|
|
151
|
+
- \`discover_personas\`: Behavioral clustering
|
|
152
|
+
- \`detect_drift\`: Long-term behavioral changes
|
|
153
|
+
|
|
154
|
+
### Session Analysis
|
|
155
|
+
- \`search_sessions\`: Natural language session search
|
|
156
|
+
- \`list_sessions\`: Filter sessions by criteria
|
|
157
|
+
- \`get_session_details\`: Session behavioral timeline
|
|
158
|
+
- \`predict_outcomes\`: Predict session outcomes
|
|
159
|
+
- \`triage_sessions\`: Auto-triage sessions with compromised UX
|
|
160
|
+
|
|
161
|
+
### Session Triage
|
|
162
|
+
Use \`triage_sessions\` to automatically identify sessions that need attention:
|
|
163
|
+
- Surfaces sessions with user comments (direct feedback)
|
|
164
|
+
- Flags high-frustration sessions with rage clicks
|
|
165
|
+
- Includes console errors as evidence
|
|
166
|
+
- Returns replay URLs for quick investigation
|
|
167
|
+
|
|
168
|
+
Severity levels:
|
|
169
|
+
- **critical**: triage_score >= 0.7 OR has comment + high frustration
|
|
170
|
+
- **high**: triage_score >= 0.5
|
|
171
|
+
- **medium**: triage_score >= 0.3
|
|
172
|
+
- **low**: triage_score < 0.3
|
|
173
|
+
|
|
174
|
+
When triaging, prioritize:
|
|
175
|
+
1. Sessions with user comments (explicit feedback is gold)
|
|
176
|
+
2. Critical severity with rage clicks (users actively struggling)
|
|
177
|
+
3. Sessions with console errors + high frustration (likely bugs)
|
|
178
|
+
|
|
179
|
+
### Working Memory
|
|
180
|
+
- \`memory_save\`, \`memory_recall\`, \`memory_list\`, \`memory_delete\`, \`memory_clear\`: Store and retrieve intermediate results`,
|
|
181
|
+
});
|
|
182
|
+
registerGetDomains(server);
|
|
183
|
+
registerGetPageMetrics(server);
|
|
184
|
+
registerGetElementFriction(server);
|
|
185
|
+
registerSearchSessions(server);
|
|
186
|
+
registerGetIssues(server);
|
|
187
|
+
registerGetSessionDetails(server);
|
|
188
|
+
registerGetPageTrends(server);
|
|
189
|
+
registerGetAnomalies(server);
|
|
190
|
+
registerGetActionableIssues(server);
|
|
191
|
+
registerGetUxHealthReport(server);
|
|
192
|
+
registerAnalyzeFlow(server);
|
|
193
|
+
registerAnalyzeFunnel(server);
|
|
194
|
+
registerGetFlowFriction(server);
|
|
195
|
+
registerGetJourneyPatterns(server);
|
|
196
|
+
registerGetDeadClicks(server);
|
|
197
|
+
registerGetConsoleErrors(server);
|
|
198
|
+
registerCompareCohorts(server);
|
|
199
|
+
registerDetectRegressions(server);
|
|
200
|
+
registerDiscoverPersonas(server);
|
|
201
|
+
registerGetFormFriction(server);
|
|
202
|
+
registerScanSite(server);
|
|
203
|
+
registerListPages(server);
|
|
204
|
+
registerComparePeriods(server);
|
|
205
|
+
registerDetectDrift(server);
|
|
206
|
+
registerPredictOutcomes(server);
|
|
207
|
+
registerMemoryTools(server);
|
|
208
|
+
registerListSessions(server);
|
|
209
|
+
registerTriageSessions(server);
|
|
210
|
+
const transport = new StdioServerTransport();
|
|
211
|
+
await server.connect(transport);
|
|
212
|
+
console.error("[MCP] Server running on stdio");
|
|
213
|
+
}
|
|
214
|
+
main().catch((err) => {
|
|
215
|
+
console.error("[MCP] Fatal error:", err);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* analyze_flow tool
|
|
3
|
+
*
|
|
4
|
+
* Analyzes user navigation flows between pages.
|
|
5
|
+
* Thin proxy to external-api /flows/analyze endpoint.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { apiPost, isApiConfigured } from "../api/client.js";
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
export function registerAnalyzeFlow(server) {
|
|
11
|
+
server.registerTool("analyze_flow", {
|
|
12
|
+
description: "Analyze user navigation flows between pages. Returns paths with steps, " +
|
|
13
|
+
"behavioral metrics (frustration, confusion), success rates, and bottlenecks. " +
|
|
14
|
+
"Use this to understand how users navigate between specific pages.",
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
start_page: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Starting page path (e.g., /pricing). Partial match supported."),
|
|
20
|
+
end_page: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Ending page path (e.g., /checkout). Partial match supported."),
|
|
24
|
+
days: z
|
|
25
|
+
.number()
|
|
26
|
+
.optional()
|
|
27
|
+
.default(7)
|
|
28
|
+
.describe("Number of days to analyze (default: 7)"),
|
|
29
|
+
}),
|
|
30
|
+
}, async ({ start_page, end_page, days, }) => {
|
|
31
|
+
if (!isApiConfigured()) {
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: "text",
|
|
36
|
+
text: JSON.stringify({ error: "API not configured" }),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (!start_page && !end_page) {
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: JSON.stringify({
|
|
48
|
+
error: "At least one of start_page or end_page is required",
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
isError: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const { data, error } = await apiPost("/flows/analyze", {
|
|
56
|
+
start_page,
|
|
57
|
+
end_page,
|
|
58
|
+
days: days ?? 7,
|
|
59
|
+
});
|
|
60
|
+
if (error) {
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text", text: JSON.stringify({ error }) }],
|
|
63
|
+
isError: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* analyze_funnel tool
|
|
3
|
+
*
|
|
4
|
+
* Analyzes conversion funnels through a defined sequence of pages.
|
|
5
|
+
* Thin proxy to external-api /flows/funnel endpoint.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { apiPost, isApiConfigured } from "../api/client.js";
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
export function registerAnalyzeFunnel(server) {
|
|
11
|
+
server.registerTool("analyze_funnel", {
|
|
12
|
+
description: "Analyze a conversion funnel through a sequence of pages. Returns per-step metrics " +
|
|
13
|
+
"(entered, converted, dropped, dwell time, frustration, confusion) and dropoff analysis " +
|
|
14
|
+
'showing where users exit. Supports wildcards: "/checkout*" matches all checkout pages.',
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
steps: z
|
|
17
|
+
.array(z.string())
|
|
18
|
+
.min(2)
|
|
19
|
+
.describe('Ordered page paths forming the funnel (e.g., ["/cart", "/checkout", "/payment"])'),
|
|
20
|
+
days: z
|
|
21
|
+
.number()
|
|
22
|
+
.optional()
|
|
23
|
+
.default(7)
|
|
24
|
+
.describe("Number of days to analyze (default: 7)"),
|
|
25
|
+
}),
|
|
26
|
+
}, async ({ steps, days }) => {
|
|
27
|
+
if (!isApiConfigured()) {
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: JSON.stringify({ error: "API not configured" }),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
isError: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (!steps || steps.length < 2) {
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: JSON.stringify({
|
|
44
|
+
error: "At least 2 steps are required for funnel analysis",
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
isError: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const { data, error } = await apiPost("/flows/funnel", {
|
|
52
|
+
steps,
|
|
53
|
+
days: days ?? 7,
|
|
54
|
+
});
|
|
55
|
+
if (error) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: JSON.stringify({ error }) }],
|
|
58
|
+
isError: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compare_cohorts tool
|
|
3
|
+
*
|
|
4
|
+
* Compare two user cohorts side by side.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { apiPost, isApiConfigured } from "../api/client.js";
|
|
8
|
+
const cohortFilterSchema = z.object({
|
|
9
|
+
device: z
|
|
10
|
+
.enum(["desktop", "tablet", "mobile"])
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("Filter by device type"),
|
|
13
|
+
outcome: z
|
|
14
|
+
.enum(["COMPLETED", "STRUGGLED", "BLOCKED", "DISENGAGED"])
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Filter by session outcome"),
|
|
17
|
+
pattern: z.string().optional().describe("Filter by behavioral pattern"),
|
|
18
|
+
page_path: z.string().optional().describe("Filter by page path"),
|
|
19
|
+
has_friction: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Filter for sessions with friction"),
|
|
23
|
+
has_rage_clicks: z
|
|
24
|
+
.boolean()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Filter for sessions with rage clicks"),
|
|
27
|
+
});
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
export function registerCompareCohorts(server) {
|
|
30
|
+
server.registerTool("compare_cohorts", {
|
|
31
|
+
description: "Compare two user cohorts side by side. Each cohort is defined by filter criteria: " +
|
|
32
|
+
"device (desktop/tablet/mobile), outcome (COMPLETED/STRUGGLED/BLOCKED/DISENGAGED), " +
|
|
33
|
+
"pattern, has_friction, has_rage_clicks, or page_path. " +
|
|
34
|
+
"Returns behavioral metrics for each cohort and statistically significant differences. " +
|
|
35
|
+
'Use this to answer "how do mobile users differ from desktop?", "what distinguishes ' +
|
|
36
|
+
'users who complete vs abandon?", or "do form users struggle more than non-form users?"',
|
|
37
|
+
inputSchema: z.object({
|
|
38
|
+
cohort_a: cohortFilterSchema.describe('Filters for cohort A, e.g. {"device": "mobile"} or {"outcome": "COMPLETED"}'),
|
|
39
|
+
cohort_b: cohortFilterSchema.describe('Filters for cohort B, e.g. {"device": "desktop"} or {"outcome": "BLOCKED"}'),
|
|
40
|
+
cohort_a_label: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Human label for cohort A"),
|
|
44
|
+
cohort_b_label: z
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Human label for cohort B"),
|
|
48
|
+
page_path: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Scope comparison to a specific page"),
|
|
52
|
+
date_from: z.string().optional().describe("Start date (ISO format)"),
|
|
53
|
+
date_to: z.string().optional().describe("End date (ISO format)"),
|
|
54
|
+
}),
|
|
55
|
+
}, async ({ cohort_a, cohort_b, cohort_a_label, cohort_b_label, page_path, date_from, date_to, }) => {
|
|
56
|
+
if (!isApiConfigured()) {
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: JSON.stringify({ error: "API not configured" }),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const { data, error } = await apiPost("/cohorts/compare", {
|
|
68
|
+
cohort_a,
|
|
69
|
+
cohort_b,
|
|
70
|
+
cohort_a_label,
|
|
71
|
+
cohort_b_label,
|
|
72
|
+
page_path,
|
|
73
|
+
date_from,
|
|
74
|
+
date_to,
|
|
75
|
+
});
|
|
76
|
+
if (error) {
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: JSON.stringify({ error }) }],
|
|
79
|
+
isError: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compare_periods tool
|
|
3
|
+
*
|
|
4
|
+
* Compare metrics between two time periods for a page.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { apiPost, isApiConfigured } from "../api/client.js";
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
export function registerComparePeriods(server) {
|
|
10
|
+
server.registerTool("compare_periods", {
|
|
11
|
+
description: "Compare behavioral metrics between two time periods for a specific page. " +
|
|
12
|
+
"Returns metrics for each period and the deltas between them. " +
|
|
13
|
+
"Use this to measure the impact of changes or compare week-over-week performance.",
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
page_path: z.string().describe("Page path to compare"),
|
|
16
|
+
period_a: z
|
|
17
|
+
.object({
|
|
18
|
+
from: z.string().describe("Start date for period A (ISO format)"),
|
|
19
|
+
to: z.string().describe("End date for period A (ISO format)"),
|
|
20
|
+
})
|
|
21
|
+
.describe("First time period"),
|
|
22
|
+
period_b: z
|
|
23
|
+
.object({
|
|
24
|
+
from: z.string().describe("Start date for period B (ISO format)"),
|
|
25
|
+
to: z.string().describe("End date for period B (ISO format)"),
|
|
26
|
+
})
|
|
27
|
+
.describe("Second time period"),
|
|
28
|
+
}),
|
|
29
|
+
}, async ({ page_path, period_a, period_b, }) => {
|
|
30
|
+
if (!isApiConfigured()) {
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: JSON.stringify({ error: "API not configured" }),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
isError: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const { data, error } = await apiPost("/pages/compare-periods", {
|
|
42
|
+
page_path,
|
|
43
|
+
period_a,
|
|
44
|
+
period_b,
|
|
45
|
+
});
|
|
46
|
+
if (error) {
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text: JSON.stringify({ error }) }],
|
|
49
|
+
isError: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detect_drift tool
|
|
3
|
+
*
|
|
4
|
+
* Detect behavioral drift over time.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { apiGet, isApiConfigured } from "../api/client.js";
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
export function registerDetectDrift(server) {
|
|
10
|
+
server.registerTool("detect_drift", {
|
|
11
|
+
description: "Detect behavioral drift over time by analyzing how user behavior patterns change. " +
|
|
12
|
+
"Splits data into time windows and measures centroid shift between consecutive periods. " +
|
|
13
|
+
"Use this for long-term monitoring to detect gradual UX degradation.",
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
page_path: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Scope drift detection to a specific page"),
|
|
19
|
+
days: z
|
|
20
|
+
.number()
|
|
21
|
+
.optional()
|
|
22
|
+
.default(30)
|
|
23
|
+
.describe("Number of days to analyze (default 30)"),
|
|
24
|
+
window_size: z
|
|
25
|
+
.number()
|
|
26
|
+
.optional()
|
|
27
|
+
.default(7)
|
|
28
|
+
.describe("Size of each time window in days (default 7)"),
|
|
29
|
+
}),
|
|
30
|
+
}, async ({ page_path, days, window_size, }) => {
|
|
31
|
+
if (!isApiConfigured()) {
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: "text",
|
|
36
|
+
text: JSON.stringify({ error: "API not configured" }),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const { data, error } = await apiGet("/drift", {
|
|
43
|
+
page_path,
|
|
44
|
+
days: days ?? 30,
|
|
45
|
+
window_size: window_size ?? 7,
|
|
46
|
+
});
|
|
47
|
+
if (error) {
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text: JSON.stringify({ error }) }],
|
|
50
|
+
isError: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
54
|
+
});
|
|
55
|
+
}
|