lcontext-mcp 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/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1084 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1084 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lcontext MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Provides page and element analytics context for Claude Code.
|
|
6
|
+
* Requires authentication via LCONTEXT_API_KEY environment variable only.
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* - get_page_context: Get page analytics, stats, and related elements for a given path and time range
|
|
10
|
+
* - list_pages: List all tracked pages for the authenticated website
|
|
11
|
+
* - get_element_context: Get detailed analytics for a specific element
|
|
12
|
+
* - get_app_context: Get application-wide analytics including sessions, visitors, and AI insights
|
|
13
|
+
* - get_visitors: Get visitors list with AI-generated profiles and segment assignments
|
|
14
|
+
* - get_visitor_detail: Get detailed profile and sessions for a specific visitor
|
|
15
|
+
* - get_sessions: Get sessions list with AI summaries and sentiment analysis
|
|
16
|
+
* - get_session_detail: Get detailed session info including events and visitor context
|
|
17
|
+
*/
|
|
18
|
+
// Handle CLI flags before importing heavy dependencies
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
21
|
+
console.log('1.1.0');
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
25
|
+
console.log(`lcontext v1.1.0
|
|
26
|
+
|
|
27
|
+
MCP server for lcontext page analytics.
|
|
28
|
+
Provides page and element context for Claude Code.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
lcontext [options]
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
-v, --version Show version number
|
|
35
|
+
-h, --help Show this help message
|
|
36
|
+
--update Update to the latest version
|
|
37
|
+
|
|
38
|
+
Environment Variables:
|
|
39
|
+
LCONTEXT_API_KEY Your lcontext API key (required)
|
|
40
|
+
LCONTEXT_API_URL API base URL (default: https://lcontext.com)
|
|
41
|
+
|
|
42
|
+
Documentation: https://lcontext.com/docs/mcp
|
|
43
|
+
`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
// Self-update command - runs async then exits
|
|
47
|
+
if (args.includes('--update')) {
|
|
48
|
+
import('fs').then(fs => import('os').then(os => {
|
|
49
|
+
const CURRENT_VERSION = '1.1.0';
|
|
50
|
+
const GITHUB_REPO = 'evan-kyr/lcontext';
|
|
51
|
+
const platform = os.platform();
|
|
52
|
+
const arch = os.arch();
|
|
53
|
+
let binaryName;
|
|
54
|
+
if (platform === 'darwin') {
|
|
55
|
+
binaryName = arch === 'arm64' ? 'lcontext-macos-arm64' : 'lcontext-macos-x64';
|
|
56
|
+
}
|
|
57
|
+
else if (platform === 'win32') {
|
|
58
|
+
binaryName = 'lcontext-windows-x64.exe';
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
binaryName = arch === 'arm64' ? 'lcontext-linux-arm64' : 'lcontext-linux-x64';
|
|
62
|
+
}
|
|
63
|
+
console.log(`Current version: ${CURRENT_VERSION}`);
|
|
64
|
+
console.log(`Platform: ${platform} ${arch}`);
|
|
65
|
+
console.log('Checking for updates...');
|
|
66
|
+
fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`, {
|
|
67
|
+
headers: { 'User-Agent': 'lcontext-mcp' }
|
|
68
|
+
})
|
|
69
|
+
.then(res => res.json())
|
|
70
|
+
.then((release) => {
|
|
71
|
+
const latestVersion = release.tag_name.replace(/^v/, '');
|
|
72
|
+
console.log(`Latest version: ${latestVersion}`);
|
|
73
|
+
if (latestVersion === CURRENT_VERSION) {
|
|
74
|
+
console.log('Already up to date!');
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
const asset = release.assets.find((a) => a.name === binaryName);
|
|
78
|
+
if (!asset) {
|
|
79
|
+
console.error(`No binary found for ${binaryName}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
console.log(`Downloading ${binaryName}...`);
|
|
83
|
+
return fetch(asset.browser_download_url, {
|
|
84
|
+
headers: { 'User-Agent': 'lcontext-mcp' }
|
|
85
|
+
}).then(res => res.arrayBuffer()).then(data => {
|
|
86
|
+
const binaryData = Buffer.from(data);
|
|
87
|
+
const execPath = process.execPath;
|
|
88
|
+
const tempPath = execPath + '.new';
|
|
89
|
+
const backupPath = execPath + '.backup';
|
|
90
|
+
console.log(`Installing to ${execPath}...`);
|
|
91
|
+
fs.writeFileSync(tempPath, binaryData);
|
|
92
|
+
fs.chmodSync(tempPath, 0o755);
|
|
93
|
+
if (fs.existsSync(backupPath))
|
|
94
|
+
fs.unlinkSync(backupPath);
|
|
95
|
+
fs.renameSync(execPath, backupPath);
|
|
96
|
+
fs.renameSync(tempPath, execPath);
|
|
97
|
+
fs.unlinkSync(backupPath);
|
|
98
|
+
console.log(`Successfully updated to ${latestVersion}!`);
|
|
99
|
+
process.exit(0);
|
|
100
|
+
});
|
|
101
|
+
})
|
|
102
|
+
.catch((err) => {
|
|
103
|
+
console.error('Update failed:', err.message);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
106
|
+
}));
|
|
107
|
+
// Keep process alive while update runs
|
|
108
|
+
setInterval(() => { }, 1000);
|
|
109
|
+
}
|
|
110
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
111
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
112
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
113
|
+
import { z } from "zod";
|
|
114
|
+
// Configuration
|
|
115
|
+
const CURRENT_VERSION = "1.1.0";
|
|
116
|
+
const GITHUB_REPO = "evan-kyr/lcontext";
|
|
117
|
+
const API_BASE_URL = process.env.LCONTEXT_API_URL || "https://lcontext.com";
|
|
118
|
+
const API_KEY = process.env.LCONTEXT_API_KEY;
|
|
119
|
+
// Check for updates (non-blocking, outputs to stderr)
|
|
120
|
+
async function checkForUpdates() {
|
|
121
|
+
try {
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
124
|
+
const response = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`, {
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
headers: { 'User-Agent': 'lcontext-mcp' }
|
|
127
|
+
});
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
if (!response.ok)
|
|
130
|
+
return;
|
|
131
|
+
const release = await response.json();
|
|
132
|
+
const latestVersion = release.tag_name.replace(/^v/, '');
|
|
133
|
+
if (latestVersion !== CURRENT_VERSION && isNewerVersion(latestVersion, CURRENT_VERSION)) {
|
|
134
|
+
console.error(`\n[lcontext] Update available: ${CURRENT_VERSION} → ${latestVersion}`);
|
|
135
|
+
console.error(`[lcontext] Download: https://github.com/${GITHUB_REPO}/releases/latest\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Silently ignore - don't block startup for update checks
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function isNewerVersion(latest, current) {
|
|
143
|
+
const latestParts = latest.split('.').map(Number);
|
|
144
|
+
const currentParts = current.split('.').map(Number);
|
|
145
|
+
for (let i = 0; i < Math.max(latestParts.length, currentParts.length); i++) {
|
|
146
|
+
const l = latestParts[i] || 0;
|
|
147
|
+
const c = currentParts[i] || 0;
|
|
148
|
+
if (l > c)
|
|
149
|
+
return true;
|
|
150
|
+
if (l < c)
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
// Run update check in background
|
|
156
|
+
checkForUpdates();
|
|
157
|
+
// Validation schemas
|
|
158
|
+
const getPageContextSchema = z.object({
|
|
159
|
+
path: z.string().describe("The page path to get context for (e.g., '/products', '/checkout')"),
|
|
160
|
+
startDate: z.string().optional().describe("Start date for stats (ISO format, e.g., '2025-01-01')"),
|
|
161
|
+
endDate: z.string().optional().describe("End date for stats (ISO format, e.g., '2025-01-13')"),
|
|
162
|
+
periodType: z.enum(["day", "week"]).optional().default("day").describe("Period type for stats aggregation")
|
|
163
|
+
});
|
|
164
|
+
const listPagesSchema = z.object({
|
|
165
|
+
limit: z.number().optional().default(50).describe("Maximum number of pages to return (max: 200)"),
|
|
166
|
+
search: z.string().optional().describe("Search filter for page paths")
|
|
167
|
+
});
|
|
168
|
+
const getElementContextSchema = z.object({
|
|
169
|
+
elementLabel: z.string().optional().describe("The element's label text or aria-label to search for"),
|
|
170
|
+
elementId: z.string().optional().describe("The element's HTML ID to search for"),
|
|
171
|
+
pagePath: z.string().optional().describe("Optional page path to filter elements")
|
|
172
|
+
});
|
|
173
|
+
const getAppContextSchema = z.object({
|
|
174
|
+
periodType: z.enum(["day", "week"]).optional().default("day").describe("Period type for stats aggregation"),
|
|
175
|
+
limit: z.number().optional().default(7).describe("Number of periods to return (default: 7, max: 30)")
|
|
176
|
+
});
|
|
177
|
+
const getVisitorsSchema = z.object({
|
|
178
|
+
limit: z.number().optional().default(20).describe("Maximum number of visitors to return (default: 20, max: 100)"),
|
|
179
|
+
offset: z.number().optional().default(0).describe("Offset for pagination"),
|
|
180
|
+
segmentId: z.number().optional().describe("Filter by segment ID"),
|
|
181
|
+
search: z.string().optional().describe("Search in visitor ID, title, summary, interests, goals, action, evidence"),
|
|
182
|
+
firstVisitAfter: z.string().optional().describe("Filter visitors who first visited after this date (ISO format)"),
|
|
183
|
+
firstVisitBefore: z.string().optional().describe("Filter visitors who first visited before this date (ISO format)"),
|
|
184
|
+
lastVisitAfter: z.string().optional().describe("Filter visitors who last visited after this date (ISO format)"),
|
|
185
|
+
lastVisitBefore: z.string().optional().describe("Filter visitors who last visited before this date (ISO format)"),
|
|
186
|
+
engagementTrend: z.enum(["increasing", "stable", "decreasing"]).optional().describe("Filter by engagement trend"),
|
|
187
|
+
overallSentiment: z.enum(["positive", "negative", "neutral", "mixed"]).optional().describe("Filter by overall sentiment")
|
|
188
|
+
});
|
|
189
|
+
const getVisitorDetailSchema = z.object({
|
|
190
|
+
visitorId: z.string().describe("The visitor's unique identifier")
|
|
191
|
+
});
|
|
192
|
+
const getSessionsSchema = z.object({
|
|
193
|
+
limit: z.number().optional().default(20).describe("Maximum number of sessions to return (default: 20, max: 100)"),
|
|
194
|
+
offset: z.number().optional().default(0).describe("Offset for pagination"),
|
|
195
|
+
visitorId: z.string().optional().describe("Filter sessions by visitor ID"),
|
|
196
|
+
sentiment: z.enum(["positive", "negative", "neutral"]).optional().describe("Filter by session sentiment"),
|
|
197
|
+
startDate: z.string().optional().describe("Start date for filtering (ISO format)"),
|
|
198
|
+
endDate: z.string().optional().describe("End date for filtering (ISO format)"),
|
|
199
|
+
search: z.string().optional().describe("Search in session title and description"),
|
|
200
|
+
minDuration: z.number().optional().describe("Filter sessions with duration >= this value (seconds)"),
|
|
201
|
+
maxDuration: z.number().optional().describe("Filter sessions with duration <= this value (seconds)"),
|
|
202
|
+
minEventsCount: z.number().optional().describe("Filter sessions with events count >= this value"),
|
|
203
|
+
maxEventsCount: z.number().optional().describe("Filter sessions with events count <= this value")
|
|
204
|
+
});
|
|
205
|
+
const getSessionDetailSchema = z.object({
|
|
206
|
+
sessionId: z.number().describe("The session's numeric ID")
|
|
207
|
+
});
|
|
208
|
+
// HTTP client helper
|
|
209
|
+
async function apiRequest(endpoint, options = {}) {
|
|
210
|
+
if (!API_KEY) {
|
|
211
|
+
throw new Error("LCONTEXT_API_KEY environment variable is required");
|
|
212
|
+
}
|
|
213
|
+
const url = `${API_BASE_URL}${endpoint}`;
|
|
214
|
+
const headers = {
|
|
215
|
+
'X-API-Key': API_KEY,
|
|
216
|
+
'Content-Type': 'application/json',
|
|
217
|
+
...options.headers
|
|
218
|
+
};
|
|
219
|
+
const response = await fetch(url, {
|
|
220
|
+
...options,
|
|
221
|
+
headers
|
|
222
|
+
});
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
const errorBody = await response.text();
|
|
225
|
+
throw new Error(`API request failed (${response.status}): ${errorBody}`);
|
|
226
|
+
}
|
|
227
|
+
const text = await response.text();
|
|
228
|
+
try {
|
|
229
|
+
return JSON.parse(text);
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
throw new Error(`Invalid JSON response from API: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Helper function to format page context
|
|
236
|
+
function formatPageContext(data) {
|
|
237
|
+
const { page, stats, elements } = data;
|
|
238
|
+
let output = `
|
|
239
|
+
## Page Analytics: ${page.path}
|
|
240
|
+
${page.title ? `**Title:** ${page.title}` : ''}
|
|
241
|
+
**First Seen:** ${new Date(page.firstSeenAt).toLocaleDateString()}
|
|
242
|
+
**Last Seen:** ${new Date(page.lastSeenAt).toLocaleDateString()}
|
|
243
|
+
|
|
244
|
+
### Page Statistics
|
|
245
|
+
`;
|
|
246
|
+
if (stats.length === 0) {
|
|
247
|
+
output += "No statistics available for the selected time range.\n";
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// Aggregate totals
|
|
251
|
+
const totals = stats.reduce((acc, stat) => ({
|
|
252
|
+
views: acc.views + (stat.viewCount || 0),
|
|
253
|
+
uniqueVisitors: acc.uniqueVisitors + (stat.uniqueVisitors || 0),
|
|
254
|
+
bounces: acc.bounces + (stat.bounceCount || 0),
|
|
255
|
+
entries: acc.entries + (stat.entryCount || 0),
|
|
256
|
+
exits: acc.exits + (stat.exitCount || 0)
|
|
257
|
+
}), { views: 0, uniqueVisitors: 0, bounces: 0, entries: 0, exits: 0 });
|
|
258
|
+
output += `
|
|
259
|
+
**Summary (${stats.length} ${stats[0]?.periodType || 'day'}s)**
|
|
260
|
+
- Total Views: ${totals.views}
|
|
261
|
+
- Total Unique Visitors: ${totals.uniqueVisitors}
|
|
262
|
+
- Total Bounces: ${totals.bounces}
|
|
263
|
+
- Entry Rate: ${totals.views > 0 ? ((totals.entries / totals.views) * 100).toFixed(1) : 0}%
|
|
264
|
+
- Exit Rate: ${totals.views > 0 ? ((totals.exits / totals.views) * 100).toFixed(1) : 0}%
|
|
265
|
+
|
|
266
|
+
**Recent Daily Breakdown:**
|
|
267
|
+
`;
|
|
268
|
+
for (const stat of stats.slice(0, 7)) {
|
|
269
|
+
const date = new Date(stat.periodStart).toLocaleDateString();
|
|
270
|
+
output += `| ${date} | Views: ${stat.viewCount} | Visitors: ${stat.uniqueVisitors} | Avg Duration: ${stat.avgDuration}s | Scroll: ${stat.avgScrollDepth}% |\n`;
|
|
271
|
+
}
|
|
272
|
+
// Add AI summaries if available
|
|
273
|
+
const summaries = stats.filter((stat) => stat.aiSummary);
|
|
274
|
+
if (summaries.length > 0) {
|
|
275
|
+
output += `\n### AI Insights\n`;
|
|
276
|
+
for (const stat of summaries) {
|
|
277
|
+
const date = new Date(stat.periodStart).toLocaleDateString();
|
|
278
|
+
const updatedAt = stat.aiSummaryUpdatedAt ? new Date(stat.aiSummaryUpdatedAt).toLocaleDateString() : 'Unknown';
|
|
279
|
+
output += `\n**${stat.periodType === 'week' ? 'Week of' : ''} ${date}** (updated: ${updatedAt})\n${stat.aiSummary}\n`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
output += `
|
|
284
|
+
### Interactive Elements (${elements.length} tracked)
|
|
285
|
+
`;
|
|
286
|
+
if (elements.length === 0) {
|
|
287
|
+
output += "No interactive elements tracked on this page.\n";
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// Calculate total interactions for each element
|
|
291
|
+
const elementsWithTotals = elements.map((element) => {
|
|
292
|
+
const totalInteractions = element.stats.reduce((sum, s) => sum + (s.interactionCount || 0), 0);
|
|
293
|
+
return { ...element, totalInteractions };
|
|
294
|
+
}).sort((a, b) => b.totalInteractions - a.totalInteractions);
|
|
295
|
+
for (const element of elementsWithTotals.slice(0, 20)) {
|
|
296
|
+
const label = element.label || element.ariaLabel || element.elementId || element.tagName || 'Unknown';
|
|
297
|
+
const category = element.category?.toUpperCase() || 'OTHER';
|
|
298
|
+
output += `
|
|
299
|
+
**${category}: ${label}**
|
|
300
|
+
- Interactions: ${element.totalInteractions}
|
|
301
|
+
- Tag: \`<${element.tagName || 'unknown'}>\`${element.elementId ? ` id="${element.elementId}"` : ''}
|
|
302
|
+
${element.destinationUrl ? `- Links to: ${element.destinationUrl}` : ''}
|
|
303
|
+
`;
|
|
304
|
+
}
|
|
305
|
+
if (elementsWithTotals.length > 20) {
|
|
306
|
+
output += `\n*...and ${elementsWithTotals.length - 20} more elements*\n`;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return output.trim();
|
|
310
|
+
}
|
|
311
|
+
// Helper function to format app context
|
|
312
|
+
function formatAppContext(data) {
|
|
313
|
+
const { stats, topPages, topEntryPages, topExitPages, recentInsights } = data;
|
|
314
|
+
let output = `## Application Analytics Overview\n`;
|
|
315
|
+
if (stats.length === 0) {
|
|
316
|
+
output += "\nNo statistics available for the selected time range.\n";
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
// Aggregate totals
|
|
320
|
+
const totals = stats.reduce((acc, stat) => ({
|
|
321
|
+
sessions: acc.sessions + (stat.totalSessions || 0),
|
|
322
|
+
uniqueVisitors: acc.uniqueVisitors + (stat.uniqueVisitors || 0),
|
|
323
|
+
newVisitors: acc.newVisitors + (stat.newVisitors || 0),
|
|
324
|
+
pageViews: acc.pageViews + (stat.totalPageViews || 0),
|
|
325
|
+
totalDuration: acc.totalDuration + (stat.totalDuration || 0),
|
|
326
|
+
totalEvents: acc.totalEvents + (stat.totalEvents || 0),
|
|
327
|
+
bounces: acc.bounces + (stat.bounceCount || 0),
|
|
328
|
+
positiveSessions: acc.positiveSessions + (stat.positiveSessions || 0),
|
|
329
|
+
negativeSessions: acc.negativeSessions + (stat.negativeSessions || 0),
|
|
330
|
+
neutralSessions: acc.neutralSessions + (stat.neutralSessions || 0)
|
|
331
|
+
}), { sessions: 0, uniqueVisitors: 0, newVisitors: 0, pageViews: 0, totalDuration: 0, totalEvents: 0, bounces: 0, positiveSessions: 0, negativeSessions: 0, neutralSessions: 0 });
|
|
332
|
+
const avgSessionDuration = totals.sessions > 0 ? Math.round(totals.totalDuration / totals.sessions) : 0;
|
|
333
|
+
const bounceRate = totals.sessions > 0 ? ((totals.bounces / totals.sessions) * 100).toFixed(1) : '0';
|
|
334
|
+
output += `
|
|
335
|
+
### Summary (${stats.length} ${stats[0]?.periodType || 'day'}s)
|
|
336
|
+
- **Total Sessions:** ${totals.sessions.toLocaleString()}
|
|
337
|
+
- **Unique Visitors:** ${totals.uniqueVisitors.toLocaleString()}
|
|
338
|
+
- **New Visitors:** ${totals.newVisitors.toLocaleString()}
|
|
339
|
+
- **Total Page Views:** ${totals.pageViews.toLocaleString()}
|
|
340
|
+
- **Avg Session Duration:** ${avgSessionDuration}s
|
|
341
|
+
- **Total Events:** ${totals.totalEvents.toLocaleString()}
|
|
342
|
+
- **Bounce Rate:** ${bounceRate}%
|
|
343
|
+
|
|
344
|
+
### Session Sentiment
|
|
345
|
+
- Positive: ${totals.positiveSessions} | Neutral: ${totals.neutralSessions} | Negative: ${totals.negativeSessions}
|
|
346
|
+
|
|
347
|
+
### Daily Breakdown
|
|
348
|
+
`;
|
|
349
|
+
for (const stat of stats.slice(0, 7)) {
|
|
350
|
+
const date = new Date(stat.periodStart).toLocaleDateString();
|
|
351
|
+
output += `| ${date} | Sessions: ${stat.totalSessions} | Visitors: ${stat.uniqueVisitors} | Page Views: ${stat.totalPageViews} | Bounce: ${stat.bounceRate}% |\n`;
|
|
352
|
+
}
|
|
353
|
+
// Add AI summaries if available
|
|
354
|
+
const summaries = stats.filter((stat) => stat.aiSummary);
|
|
355
|
+
if (summaries.length > 0) {
|
|
356
|
+
output += `\n### AI Insights\n`;
|
|
357
|
+
for (const stat of summaries.slice(0, 3)) {
|
|
358
|
+
const date = new Date(stat.periodStart).toLocaleDateString();
|
|
359
|
+
output += `\n**${stat.periodType === 'week' ? 'Week of' : ''} ${date}**\n${stat.aiSummary}\n`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Top pages
|
|
364
|
+
if (topPages && topPages.length > 0) {
|
|
365
|
+
output += `\n### Top Pages by Views\n`;
|
|
366
|
+
for (const page of topPages.slice(0, 5)) {
|
|
367
|
+
output += `- ${page.path}: ${page.viewCount} views\n`;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Top entry pages
|
|
371
|
+
if (topEntryPages && topEntryPages.length > 0) {
|
|
372
|
+
output += `\n### Top Entry Pages\n`;
|
|
373
|
+
for (const page of topEntryPages.slice(0, 5)) {
|
|
374
|
+
output += `- ${page.path}: ${page.count} entries\n`;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Top exit pages
|
|
378
|
+
if (topExitPages && topExitPages.length > 0) {
|
|
379
|
+
output += `\n### Top Exit Pages\n`;
|
|
380
|
+
for (const page of topExitPages.slice(0, 5)) {
|
|
381
|
+
output += `- ${page.path}: ${page.count} exits\n`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Recent insights
|
|
385
|
+
if (recentInsights && recentInsights.length > 0) {
|
|
386
|
+
output += `\n### Recent Insights\n`;
|
|
387
|
+
for (const insight of recentInsights.slice(0, 3)) {
|
|
388
|
+
const date = new Date(insight.createdAt).toLocaleDateString();
|
|
389
|
+
output += `\n**${insight.title}** (${date})\n${insight.content}\n`;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return output.trim();
|
|
393
|
+
}
|
|
394
|
+
// Helper function to format visitors list
|
|
395
|
+
function formatVisitors(data) {
|
|
396
|
+
const { visitors, total, limit, offset } = data;
|
|
397
|
+
let output = `## Visitors\n\n`;
|
|
398
|
+
output += `Showing ${visitors.length} of ${total} visitors (offset: ${offset}):\n`;
|
|
399
|
+
if (visitors.length === 0) {
|
|
400
|
+
output += "\nNo visitors found matching the criteria.\n";
|
|
401
|
+
return output.trim();
|
|
402
|
+
}
|
|
403
|
+
for (const visitor of visitors) {
|
|
404
|
+
const sentiment = visitor.overallSentiment ? ` | ${visitor.overallSentiment}` : '';
|
|
405
|
+
const trend = visitor.engagementTrend ? ` | Trend: ${visitor.engagementTrend}` : '';
|
|
406
|
+
const segment = visitor.segmentName ? ` | Segment: ${visitor.segmentName}` : '';
|
|
407
|
+
output += `
|
|
408
|
+
### ${visitor.profileTitle || visitor.visitorId}
|
|
409
|
+
- **Visitor ID:** ${visitor.visitorId}
|
|
410
|
+
- **Sessions:** ${visitor.sessionCount}${sentiment}${trend}${segment}
|
|
411
|
+
- **First Visit:** ${new Date(visitor.firstVisitAt).toLocaleDateString()}
|
|
412
|
+
- **Last Visit:** ${new Date(visitor.lastVisitAt).toLocaleDateString()}
|
|
413
|
+
`;
|
|
414
|
+
if (visitor.profileSummary) {
|
|
415
|
+
output += `- **Profile:** ${visitor.profileSummary}\n`;
|
|
416
|
+
}
|
|
417
|
+
if (visitor.primaryInterests && visitor.primaryInterests.length > 0) {
|
|
418
|
+
output += `- **Interests:** ${visitor.primaryInterests.join(', ')}\n`;
|
|
419
|
+
}
|
|
420
|
+
if (visitor.recommendedAction) {
|
|
421
|
+
output += `- **Recommended Action:** ${visitor.recommendedAction}\n`;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return output.trim();
|
|
425
|
+
}
|
|
426
|
+
// Helper function to format visitor detail
|
|
427
|
+
function formatVisitorDetail(data) {
|
|
428
|
+
const { visitor, recentSessions } = data;
|
|
429
|
+
let output = `## Visitor Profile: ${visitor.profileTitle || visitor.visitorId}\n`;
|
|
430
|
+
output += `
|
|
431
|
+
**Visitor ID:** ${visitor.visitorId}
|
|
432
|
+
**First Visit:** ${new Date(visitor.firstVisitAt).toLocaleDateString()}
|
|
433
|
+
**Last Visit:** ${new Date(visitor.lastVisitAt).toLocaleDateString()}
|
|
434
|
+
`;
|
|
435
|
+
if (visitor.segmentName) {
|
|
436
|
+
output += `**Segment:** ${visitor.segmentName}\n`;
|
|
437
|
+
}
|
|
438
|
+
if (visitor.overallSentiment) {
|
|
439
|
+
output += `**Overall Sentiment:** ${visitor.overallSentiment}\n`;
|
|
440
|
+
}
|
|
441
|
+
if (visitor.engagementTrend) {
|
|
442
|
+
output += `**Engagement Trend:** ${visitor.engagementTrend}\n`;
|
|
443
|
+
}
|
|
444
|
+
if (visitor.profileSummary) {
|
|
445
|
+
output += `\n### Profile Summary\n${visitor.profileSummary}\n`;
|
|
446
|
+
}
|
|
447
|
+
if (visitor.primaryInterests && visitor.primaryInterests.length > 0) {
|
|
448
|
+
output += `\n### Primary Interests\n`;
|
|
449
|
+
for (const interest of visitor.primaryInterests) {
|
|
450
|
+
output += `- ${interest}\n`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (visitor.goalsInferred && visitor.goalsInferred.length > 0) {
|
|
454
|
+
output += `\n### Inferred Goals\n`;
|
|
455
|
+
for (const goal of visitor.goalsInferred) {
|
|
456
|
+
output += `- ${goal}\n`;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (visitor.recommendedAction) {
|
|
460
|
+
output += `\n### Recommended Action\n${visitor.recommendedAction}\n`;
|
|
461
|
+
}
|
|
462
|
+
if (visitor.evidence && visitor.evidence.length > 0) {
|
|
463
|
+
output += `\n### Supporting Evidence\n`;
|
|
464
|
+
for (const e of visitor.evidence.slice(0, 5)) {
|
|
465
|
+
output += `- ${e}\n`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (recentSessions && recentSessions.length > 0) {
|
|
469
|
+
output += `\n### Recent Sessions (${recentSessions.length})\n`;
|
|
470
|
+
for (const session of recentSessions) {
|
|
471
|
+
const date = new Date(session.startTime).toLocaleDateString();
|
|
472
|
+
const sentiment = session.sentiment ? ` [${session.sentiment}]` : '';
|
|
473
|
+
output += `\n**Session ${session.id}** - ${date}${sentiment}\n`;
|
|
474
|
+
if (session.title) {
|
|
475
|
+
output += `Title: ${session.title}\n`;
|
|
476
|
+
}
|
|
477
|
+
if (session.description) {
|
|
478
|
+
output += `${session.description}\n`;
|
|
479
|
+
}
|
|
480
|
+
output += `Duration: ${session.duration || 0}s | Events: ${session.eventsCount || 0}\n`;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return output.trim();
|
|
484
|
+
}
|
|
485
|
+
// Helper function to format sessions list
|
|
486
|
+
function formatSessions(data) {
|
|
487
|
+
const { sessions, total, limit, offset } = data;
|
|
488
|
+
let output = `## Sessions\n\n`;
|
|
489
|
+
output += `Showing ${sessions.length} of ${total} sessions (offset: ${offset}):\n`;
|
|
490
|
+
if (sessions.length === 0) {
|
|
491
|
+
output += "\nNo sessions found matching the criteria.\n";
|
|
492
|
+
return output.trim();
|
|
493
|
+
}
|
|
494
|
+
for (const session of sessions) {
|
|
495
|
+
const date = new Date(session.startTime).toLocaleDateString();
|
|
496
|
+
const time = new Date(session.startTime).toLocaleTimeString();
|
|
497
|
+
const sentiment = session.sentiment ? ` [${session.sentiment}]` : '';
|
|
498
|
+
output += `
|
|
499
|
+
### Session ${session.id} - ${date} ${time}${sentiment}
|
|
500
|
+
- **Visitor:** ${session.visitorId}
|
|
501
|
+
- **Duration:** ${session.duration || 0}s
|
|
502
|
+
- **Events:** ${session.eventsCount || 0}
|
|
503
|
+
`;
|
|
504
|
+
if (session.title) {
|
|
505
|
+
output += `- **Title:** ${session.title}\n`;
|
|
506
|
+
}
|
|
507
|
+
if (session.description) {
|
|
508
|
+
output += `- **Description:** ${session.description}\n`;
|
|
509
|
+
}
|
|
510
|
+
if (session.summary) {
|
|
511
|
+
output += `- **Summary:** ${session.summary}\n`;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return output.trim();
|
|
515
|
+
}
|
|
516
|
+
// Helper function to format session detail
|
|
517
|
+
function formatSessionDetail(data) {
|
|
518
|
+
const { session, visitor } = data;
|
|
519
|
+
const date = new Date(session.startTime).toLocaleDateString();
|
|
520
|
+
const time = new Date(session.startTime).toLocaleTimeString();
|
|
521
|
+
const sentiment = session.sentiment ? ` [${session.sentiment}]` : '';
|
|
522
|
+
let output = `## Session ${session.id}${sentiment}\n`;
|
|
523
|
+
output += `**Date:** ${date} ${time}\n`;
|
|
524
|
+
output += `**Duration:** ${session.duration || 0}s\n`;
|
|
525
|
+
output += `**Events:** ${session.eventsCount || 0}\n`;
|
|
526
|
+
if (session.title) {
|
|
527
|
+
output += `**Title:** ${session.title}\n`;
|
|
528
|
+
}
|
|
529
|
+
if (visitor) {
|
|
530
|
+
output += `\n### Visitor Context\n`;
|
|
531
|
+
output += `**Visitor ID:** ${visitor.visitorId}\n`;
|
|
532
|
+
if (visitor.profileTitle) {
|
|
533
|
+
output += `**Profile:** ${visitor.profileTitle}\n`;
|
|
534
|
+
}
|
|
535
|
+
if (visitor.profileSummary) {
|
|
536
|
+
output += `**Summary:** ${visitor.profileSummary}\n`;
|
|
537
|
+
}
|
|
538
|
+
if (visitor.overallSentiment) {
|
|
539
|
+
output += `**Overall Sentiment:** ${visitor.overallSentiment}\n`;
|
|
540
|
+
}
|
|
541
|
+
if (visitor.segmentName) {
|
|
542
|
+
output += `**Segment:** ${visitor.segmentName}\n`;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (session.description) {
|
|
546
|
+
output += `\n### Session Description\n${session.description}\n`;
|
|
547
|
+
}
|
|
548
|
+
if (session.summary) {
|
|
549
|
+
output += `\n### AI Summary\n${session.summary}\n`;
|
|
550
|
+
}
|
|
551
|
+
if (session.events && Array.isArray(session.events) && session.events.length > 0) {
|
|
552
|
+
output += `\n### Events Timeline (${session.events.length} events)\n`;
|
|
553
|
+
// Group events by type for a summary
|
|
554
|
+
const eventTypes = {};
|
|
555
|
+
for (const event of session.events) {
|
|
556
|
+
const type = event.type || 'unknown';
|
|
557
|
+
eventTypes[type] = (eventTypes[type] || 0) + 1;
|
|
558
|
+
}
|
|
559
|
+
output += `**Event Summary:** `;
|
|
560
|
+
output += Object.entries(eventTypes)
|
|
561
|
+
.map(([type, count]) => `${type}: ${count}`)
|
|
562
|
+
.join(', ');
|
|
563
|
+
output += '\n\n';
|
|
564
|
+
// Show first 20 events with details
|
|
565
|
+
const eventsToShow = session.events.slice(0, 20);
|
|
566
|
+
for (const event of eventsToShow) {
|
|
567
|
+
const eventTime = event.timestamp ? new Date(event.timestamp).toLocaleTimeString() : '';
|
|
568
|
+
output += `- **${event.type}**${eventTime ? ` (${eventTime})` : ''}`;
|
|
569
|
+
if (event.data) {
|
|
570
|
+
if (event.data.path)
|
|
571
|
+
output += ` - ${event.data.path}`;
|
|
572
|
+
if (event.data.elementId)
|
|
573
|
+
output += ` [#${event.data.elementId}]`;
|
|
574
|
+
if (event.data.label)
|
|
575
|
+
output += ` "${event.data.label}"`;
|
|
576
|
+
}
|
|
577
|
+
output += '\n';
|
|
578
|
+
}
|
|
579
|
+
if (session.events.length > 20) {
|
|
580
|
+
output += `\n*... and ${session.events.length - 20} more events*\n`;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return output.trim();
|
|
584
|
+
}
|
|
585
|
+
// Initialize MCP server
|
|
586
|
+
const server = new Server({
|
|
587
|
+
name: "lcontext-mcp",
|
|
588
|
+
version: "1.0.0"
|
|
589
|
+
}, {
|
|
590
|
+
capabilities: {
|
|
591
|
+
tools: {}
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
// List available tools
|
|
595
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
596
|
+
return {
|
|
597
|
+
tools: [
|
|
598
|
+
{
|
|
599
|
+
name: "get_page_context",
|
|
600
|
+
description: "Get comprehensive analytics context for a page including stats, visitor metrics, and all interactive elements with their engagement data. Use this when analyzing user behavior on a specific page.",
|
|
601
|
+
inputSchema: {
|
|
602
|
+
type: "object",
|
|
603
|
+
properties: {
|
|
604
|
+
path: {
|
|
605
|
+
type: "string",
|
|
606
|
+
description: "The page path to get context for (e.g., '/products', '/checkout')"
|
|
607
|
+
},
|
|
608
|
+
startDate: {
|
|
609
|
+
type: "string",
|
|
610
|
+
description: "Start date for stats (ISO format, e.g., '2025-01-01')"
|
|
611
|
+
},
|
|
612
|
+
endDate: {
|
|
613
|
+
type: "string",
|
|
614
|
+
description: "End date for stats (ISO format, e.g., '2025-01-13')"
|
|
615
|
+
},
|
|
616
|
+
periodType: {
|
|
617
|
+
type: "string",
|
|
618
|
+
enum: ["day", "week"],
|
|
619
|
+
description: "Period type for stats aggregation (default: 'day')"
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
required: ["path"]
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: "list_pages",
|
|
627
|
+
description: "List all tracked pages for your website. Use this to discover available pages before getting detailed context.",
|
|
628
|
+
inputSchema: {
|
|
629
|
+
type: "object",
|
|
630
|
+
properties: {
|
|
631
|
+
limit: {
|
|
632
|
+
type: "number",
|
|
633
|
+
description: "Maximum number of pages to return (default: 50, max: 200)"
|
|
634
|
+
},
|
|
635
|
+
search: {
|
|
636
|
+
type: "string",
|
|
637
|
+
description: "Search filter for page paths (e.g., '/product' to find all product pages)"
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: "get_element_context",
|
|
644
|
+
description: "Get detailed analytics for a specific interactive element by its label or ID. Use this to understand how users interact with buttons, links, or forms.",
|
|
645
|
+
inputSchema: {
|
|
646
|
+
type: "object",
|
|
647
|
+
properties: {
|
|
648
|
+
elementLabel: {
|
|
649
|
+
type: "string",
|
|
650
|
+
description: "The element's label text or aria-label to search for"
|
|
651
|
+
},
|
|
652
|
+
elementId: {
|
|
653
|
+
type: "string",
|
|
654
|
+
description: "The element's HTML ID to search for"
|
|
655
|
+
},
|
|
656
|
+
pagePath: {
|
|
657
|
+
type: "string",
|
|
658
|
+
description: "Optional page path to filter elements"
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
name: "get_app_context",
|
|
665
|
+
description: "Get application-wide analytics context including total sessions, visitors, page views, engagement metrics, and AI-generated insights. Use this to understand overall app performance and trends.",
|
|
666
|
+
inputSchema: {
|
|
667
|
+
type: "object",
|
|
668
|
+
properties: {
|
|
669
|
+
periodType: {
|
|
670
|
+
type: "string",
|
|
671
|
+
enum: ["day", "week"],
|
|
672
|
+
description: "Period type for stats aggregation (default: 'day')"
|
|
673
|
+
},
|
|
674
|
+
limit: {
|
|
675
|
+
type: "number",
|
|
676
|
+
description: "Number of periods to return (default: 7, max: 30)"
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
name: "get_visitors",
|
|
683
|
+
description: "Get a list of visitors with their AI-generated profiles, interests, engagement trends, and segment assignments. Use this to understand who is using your application.",
|
|
684
|
+
inputSchema: {
|
|
685
|
+
type: "object",
|
|
686
|
+
properties: {
|
|
687
|
+
limit: {
|
|
688
|
+
type: "number",
|
|
689
|
+
description: "Maximum number of visitors to return (default: 20, max: 100)"
|
|
690
|
+
},
|
|
691
|
+
offset: {
|
|
692
|
+
type: "number",
|
|
693
|
+
description: "Offset for pagination (default: 0)"
|
|
694
|
+
},
|
|
695
|
+
segmentId: {
|
|
696
|
+
type: "number",
|
|
697
|
+
description: "Filter by segment ID"
|
|
698
|
+
},
|
|
699
|
+
search: {
|
|
700
|
+
type: "string",
|
|
701
|
+
description: "Search in visitor ID, title, summary, interests, goals, action, evidence"
|
|
702
|
+
},
|
|
703
|
+
firstVisitAfter: {
|
|
704
|
+
type: "string",
|
|
705
|
+
description: "Filter visitors who first visited after this date (ISO format)"
|
|
706
|
+
},
|
|
707
|
+
firstVisitBefore: {
|
|
708
|
+
type: "string",
|
|
709
|
+
description: "Filter visitors who first visited before this date (ISO format)"
|
|
710
|
+
},
|
|
711
|
+
lastVisitAfter: {
|
|
712
|
+
type: "string",
|
|
713
|
+
description: "Filter visitors who last visited after this date (ISO format)"
|
|
714
|
+
},
|
|
715
|
+
lastVisitBefore: {
|
|
716
|
+
type: "string",
|
|
717
|
+
description: "Filter visitors who last visited before this date (ISO format)"
|
|
718
|
+
},
|
|
719
|
+
engagementTrend: {
|
|
720
|
+
type: "string",
|
|
721
|
+
enum: ["increasing", "stable", "decreasing"],
|
|
722
|
+
description: "Filter by engagement trend"
|
|
723
|
+
},
|
|
724
|
+
overallSentiment: {
|
|
725
|
+
type: "string",
|
|
726
|
+
enum: ["positive", "negative", "neutral", "mixed"],
|
|
727
|
+
description: "Filter by overall sentiment"
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
name: "get_visitor_detail",
|
|
734
|
+
description: "Get detailed profile and recent sessions for a specific visitor. Use this to understand individual user behavior and journey.",
|
|
735
|
+
inputSchema: {
|
|
736
|
+
type: "object",
|
|
737
|
+
properties: {
|
|
738
|
+
visitorId: {
|
|
739
|
+
type: "string",
|
|
740
|
+
description: "The visitor's unique identifier"
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
required: ["visitorId"]
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: "get_sessions",
|
|
748
|
+
description: "Get a list of user sessions with AI-generated summaries, titles, and sentiment analysis. Use this to understand user activity patterns.",
|
|
749
|
+
inputSchema: {
|
|
750
|
+
type: "object",
|
|
751
|
+
properties: {
|
|
752
|
+
limit: {
|
|
753
|
+
type: "number",
|
|
754
|
+
description: "Maximum number of sessions to return (default: 20, max: 100)"
|
|
755
|
+
},
|
|
756
|
+
offset: {
|
|
757
|
+
type: "number",
|
|
758
|
+
description: "Offset for pagination (default: 0)"
|
|
759
|
+
},
|
|
760
|
+
visitorId: {
|
|
761
|
+
type: "string",
|
|
762
|
+
description: "Filter sessions by visitor ID"
|
|
763
|
+
},
|
|
764
|
+
sentiment: {
|
|
765
|
+
type: "string",
|
|
766
|
+
enum: ["positive", "negative", "neutral"],
|
|
767
|
+
description: "Filter by session sentiment"
|
|
768
|
+
},
|
|
769
|
+
startDate: {
|
|
770
|
+
type: "string",
|
|
771
|
+
description: "Start date for filtering (ISO format, e.g., '2025-01-01')"
|
|
772
|
+
},
|
|
773
|
+
endDate: {
|
|
774
|
+
type: "string",
|
|
775
|
+
description: "End date for filtering (ISO format, e.g., '2025-01-15')"
|
|
776
|
+
},
|
|
777
|
+
search: {
|
|
778
|
+
type: "string",
|
|
779
|
+
description: "Search in session title and description"
|
|
780
|
+
},
|
|
781
|
+
minDuration: {
|
|
782
|
+
type: "number",
|
|
783
|
+
description: "Filter sessions with duration >= this value (seconds)"
|
|
784
|
+
},
|
|
785
|
+
maxDuration: {
|
|
786
|
+
type: "number",
|
|
787
|
+
description: "Filter sessions with duration <= this value (seconds)"
|
|
788
|
+
},
|
|
789
|
+
minEventsCount: {
|
|
790
|
+
type: "number",
|
|
791
|
+
description: "Filter sessions with events count >= this value"
|
|
792
|
+
},
|
|
793
|
+
maxEventsCount: {
|
|
794
|
+
type: "number",
|
|
795
|
+
description: "Filter sessions with events count <= this value"
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
name: "get_session_detail",
|
|
802
|
+
description: "Get detailed information about a specific session including full event data and visitor context. Use this to investigate specific user interactions.",
|
|
803
|
+
inputSchema: {
|
|
804
|
+
type: "object",
|
|
805
|
+
properties: {
|
|
806
|
+
sessionId: {
|
|
807
|
+
type: "number",
|
|
808
|
+
description: "The session's numeric ID"
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
required: ["sessionId"]
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
]
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
// Handle tool calls
|
|
818
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
819
|
+
try {
|
|
820
|
+
// GET_PAGE_CONTEXT tool
|
|
821
|
+
if (request.params.name === "get_page_context") {
|
|
822
|
+
const args = getPageContextSchema.parse(request.params.arguments);
|
|
823
|
+
// Build query parameters
|
|
824
|
+
const params = new URLSearchParams();
|
|
825
|
+
if (args.startDate)
|
|
826
|
+
params.append('startDate', args.startDate);
|
|
827
|
+
if (args.endDate)
|
|
828
|
+
params.append('endDate', args.endDate);
|
|
829
|
+
if (args.periodType)
|
|
830
|
+
params.append('periodType', args.periodType);
|
|
831
|
+
// URL-encode the path to handle "/" and special characters correctly
|
|
832
|
+
// The path is encoded so "/" becomes "%2F" allowing the API to distinguish paths
|
|
833
|
+
const encodedPath = encodeURIComponent(args.path);
|
|
834
|
+
const queryString = params.toString();
|
|
835
|
+
const endpoint = `/api/mcp/pages/${encodedPath}${queryString ? `?${queryString}` : ''}`;
|
|
836
|
+
const data = await apiRequest(endpoint);
|
|
837
|
+
const contextOutput = formatPageContext(data);
|
|
838
|
+
return {
|
|
839
|
+
content: [{
|
|
840
|
+
type: "text",
|
|
841
|
+
text: contextOutput
|
|
842
|
+
}]
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
// LIST_PAGES tool
|
|
846
|
+
if (request.params.name === "list_pages") {
|
|
847
|
+
const args = listPagesSchema.parse(request.params.arguments || {});
|
|
848
|
+
const params = new URLSearchParams();
|
|
849
|
+
if (args.limit)
|
|
850
|
+
params.append('limit', args.limit.toString());
|
|
851
|
+
if (args.search)
|
|
852
|
+
params.append('search', args.search);
|
|
853
|
+
const queryString = params.toString();
|
|
854
|
+
const endpoint = `/api/mcp/pages${queryString ? `?${queryString}` : ''}`;
|
|
855
|
+
const data = await apiRequest(endpoint);
|
|
856
|
+
if (data.pages.length === 0) {
|
|
857
|
+
return {
|
|
858
|
+
content: [{
|
|
859
|
+
type: "text",
|
|
860
|
+
text: `No pages found. Make sure tracking is set up and data has been collected.`
|
|
861
|
+
}]
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
let output = `## Tracked Pages\n\n`;
|
|
865
|
+
output += `Found ${data.pages.length} pages${data.total > data.pages.length ? ` (showing first ${data.pages.length} of ${data.total})` : ''}:\n\n`;
|
|
866
|
+
for (const page of data.pages) {
|
|
867
|
+
const lastSeen = new Date(page.lastSeenAt).toLocaleDateString();
|
|
868
|
+
output += `- **${page.path}**${page.title ? ` - ${page.title}` : ''} (last seen: ${lastSeen})\n`;
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
content: [{
|
|
872
|
+
type: "text",
|
|
873
|
+
text: output.trim()
|
|
874
|
+
}]
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
// GET_ELEMENT_CONTEXT tool
|
|
878
|
+
if (request.params.name === "get_element_context") {
|
|
879
|
+
const args = getElementContextSchema.parse(request.params.arguments || {});
|
|
880
|
+
if (!args.elementLabel && !args.elementId) {
|
|
881
|
+
return {
|
|
882
|
+
isError: true,
|
|
883
|
+
content: [{
|
|
884
|
+
type: "text",
|
|
885
|
+
text: "Either elementLabel or elementId is required"
|
|
886
|
+
}]
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
const params = new URLSearchParams();
|
|
890
|
+
if (args.elementLabel)
|
|
891
|
+
params.append('elementLabel', args.elementLabel);
|
|
892
|
+
if (args.elementId)
|
|
893
|
+
params.append('elementId', args.elementId);
|
|
894
|
+
if (args.pagePath)
|
|
895
|
+
params.append('pagePath', args.pagePath);
|
|
896
|
+
const queryString = params.toString();
|
|
897
|
+
const endpoint = `/api/mcp/elements?${queryString}`;
|
|
898
|
+
const data = await apiRequest(endpoint);
|
|
899
|
+
if (data.elements.length === 0) {
|
|
900
|
+
return {
|
|
901
|
+
content: [{
|
|
902
|
+
type: "text",
|
|
903
|
+
text: `No elements found matching the criteria.`
|
|
904
|
+
}]
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
let output = `## Elements Found\n\nFound ${data.elements.length} matching element(s):\n`;
|
|
908
|
+
for (const element of data.elements) {
|
|
909
|
+
output += `
|
|
910
|
+
### ${element.category?.toUpperCase() || 'ELEMENT'}: ${element.label || element.elementId || 'Unknown'}
|
|
911
|
+
- **Page:** ${element.pagePath}
|
|
912
|
+
- **Tag:** \`<${element.tagName || 'unknown'}>\`
|
|
913
|
+
- **HTML ID:** ${element.elementId || 'N/A'}
|
|
914
|
+
- **Name:** ${element.elementName || 'N/A'}
|
|
915
|
+
- **ARIA Label:** ${element.ariaLabel || 'N/A'}
|
|
916
|
+
- **Category:** ${element.category || 'N/A'}
|
|
917
|
+
${element.destinationUrl ? `- **Links to:** ${element.destinationUrl}` : ''}
|
|
918
|
+
- **Total Interactions:** ${element.totalInteractions}
|
|
919
|
+
- **Unique Visitors:** ${element.uniqueVisitors}
|
|
920
|
+
- **First Seen:** ${new Date(element.firstSeenAt).toLocaleDateString()}
|
|
921
|
+
- **Last Seen:** ${new Date(element.lastSeenAt).toLocaleDateString()}
|
|
922
|
+
`;
|
|
923
|
+
}
|
|
924
|
+
return {
|
|
925
|
+
content: [{
|
|
926
|
+
type: "text",
|
|
927
|
+
text: output.trim()
|
|
928
|
+
}]
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
// GET_APP_CONTEXT tool
|
|
932
|
+
if (request.params.name === "get_app_context") {
|
|
933
|
+
const args = getAppContextSchema.parse(request.params.arguments || {});
|
|
934
|
+
const params = new URLSearchParams();
|
|
935
|
+
if (args.periodType)
|
|
936
|
+
params.append('periodType', args.periodType);
|
|
937
|
+
if (args.limit)
|
|
938
|
+
params.append('limit', args.limit.toString());
|
|
939
|
+
const queryString = params.toString();
|
|
940
|
+
const endpoint = `/api/mcp/app-context${queryString ? `?${queryString}` : ''}`;
|
|
941
|
+
const data = await apiRequest(endpoint);
|
|
942
|
+
const contextOutput = formatAppContext(data);
|
|
943
|
+
return {
|
|
944
|
+
content: [{
|
|
945
|
+
type: "text",
|
|
946
|
+
text: contextOutput
|
|
947
|
+
}]
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
// GET_VISITORS tool
|
|
951
|
+
if (request.params.name === "get_visitors") {
|
|
952
|
+
const args = getVisitorsSchema.parse(request.params.arguments || {});
|
|
953
|
+
const params = new URLSearchParams();
|
|
954
|
+
if (args.limit)
|
|
955
|
+
params.append('limit', args.limit.toString());
|
|
956
|
+
if (args.offset)
|
|
957
|
+
params.append('offset', args.offset.toString());
|
|
958
|
+
if (args.segmentId)
|
|
959
|
+
params.append('segmentId', args.segmentId.toString());
|
|
960
|
+
if (args.search)
|
|
961
|
+
params.append('search', args.search);
|
|
962
|
+
if (args.firstVisitAfter)
|
|
963
|
+
params.append('firstVisitAfter', args.firstVisitAfter);
|
|
964
|
+
if (args.firstVisitBefore)
|
|
965
|
+
params.append('firstVisitBefore', args.firstVisitBefore);
|
|
966
|
+
if (args.lastVisitAfter)
|
|
967
|
+
params.append('lastVisitAfter', args.lastVisitAfter);
|
|
968
|
+
if (args.lastVisitBefore)
|
|
969
|
+
params.append('lastVisitBefore', args.lastVisitBefore);
|
|
970
|
+
if (args.engagementTrend)
|
|
971
|
+
params.append('engagementTrend', args.engagementTrend);
|
|
972
|
+
if (args.overallSentiment)
|
|
973
|
+
params.append('overallSentiment', args.overallSentiment);
|
|
974
|
+
const queryString = params.toString();
|
|
975
|
+
const endpoint = `/api/mcp/visitors${queryString ? `?${queryString}` : ''}`;
|
|
976
|
+
const data = await apiRequest(endpoint);
|
|
977
|
+
const contextOutput = formatVisitors(data);
|
|
978
|
+
return {
|
|
979
|
+
content: [{
|
|
980
|
+
type: "text",
|
|
981
|
+
text: contextOutput
|
|
982
|
+
}]
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
// GET_VISITOR_DETAIL tool
|
|
986
|
+
if (request.params.name === "get_visitor_detail") {
|
|
987
|
+
const args = getVisitorDetailSchema.parse(request.params.arguments || {});
|
|
988
|
+
const endpoint = `/api/mcp/visitors/${encodeURIComponent(args.visitorId)}`;
|
|
989
|
+
const data = await apiRequest(endpoint);
|
|
990
|
+
const contextOutput = formatVisitorDetail(data);
|
|
991
|
+
return {
|
|
992
|
+
content: [{
|
|
993
|
+
type: "text",
|
|
994
|
+
text: contextOutput
|
|
995
|
+
}]
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
// GET_SESSIONS tool
|
|
999
|
+
if (request.params.name === "get_sessions") {
|
|
1000
|
+
const args = getSessionsSchema.parse(request.params.arguments || {});
|
|
1001
|
+
const params = new URLSearchParams();
|
|
1002
|
+
if (args.limit)
|
|
1003
|
+
params.append('limit', args.limit.toString());
|
|
1004
|
+
if (args.offset)
|
|
1005
|
+
params.append('offset', args.offset.toString());
|
|
1006
|
+
if (args.visitorId)
|
|
1007
|
+
params.append('visitorId', args.visitorId);
|
|
1008
|
+
if (args.sentiment)
|
|
1009
|
+
params.append('sentiment', args.sentiment);
|
|
1010
|
+
if (args.startDate)
|
|
1011
|
+
params.append('startDate', args.startDate);
|
|
1012
|
+
if (args.endDate)
|
|
1013
|
+
params.append('endDate', args.endDate);
|
|
1014
|
+
if (args.search)
|
|
1015
|
+
params.append('search', args.search);
|
|
1016
|
+
if (args.minDuration !== undefined)
|
|
1017
|
+
params.append('minDuration', args.minDuration.toString());
|
|
1018
|
+
if (args.maxDuration !== undefined)
|
|
1019
|
+
params.append('maxDuration', args.maxDuration.toString());
|
|
1020
|
+
if (args.minEventsCount !== undefined)
|
|
1021
|
+
params.append('minEventsCount', args.minEventsCount.toString());
|
|
1022
|
+
if (args.maxEventsCount !== undefined)
|
|
1023
|
+
params.append('maxEventsCount', args.maxEventsCount.toString());
|
|
1024
|
+
const queryString = params.toString();
|
|
1025
|
+
const endpoint = `/api/mcp/sessions${queryString ? `?${queryString}` : ''}`;
|
|
1026
|
+
const data = await apiRequest(endpoint);
|
|
1027
|
+
const contextOutput = formatSessions(data);
|
|
1028
|
+
return {
|
|
1029
|
+
content: [{
|
|
1030
|
+
type: "text",
|
|
1031
|
+
text: contextOutput
|
|
1032
|
+
}]
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
// GET_SESSION_DETAIL tool
|
|
1036
|
+
if (request.params.name === "get_session_detail") {
|
|
1037
|
+
const args = getSessionDetailSchema.parse(request.params.arguments || {});
|
|
1038
|
+
const endpoint = `/api/mcp/sessions/${args.sessionId}`;
|
|
1039
|
+
const data = await apiRequest(endpoint);
|
|
1040
|
+
const contextOutput = formatSessionDetail(data);
|
|
1041
|
+
return {
|
|
1042
|
+
content: [{
|
|
1043
|
+
type: "text",
|
|
1044
|
+
text: contextOutput
|
|
1045
|
+
}]
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
isError: true,
|
|
1050
|
+
content: [{
|
|
1051
|
+
type: "text",
|
|
1052
|
+
text: `Unknown tool: ${request.params.name}`
|
|
1053
|
+
}]
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
return {
|
|
1058
|
+
isError: true,
|
|
1059
|
+
content: [{
|
|
1060
|
+
type: "text",
|
|
1061
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
1062
|
+
}]
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
// Start server
|
|
1067
|
+
async function main() {
|
|
1068
|
+
if (!API_KEY) {
|
|
1069
|
+
console.error("LCONTEXT_API_KEY environment variable is required");
|
|
1070
|
+
console.error("Get your API key from: https://lcontext.com/settings");
|
|
1071
|
+
process.exit(1);
|
|
1072
|
+
}
|
|
1073
|
+
const transport = new StdioServerTransport();
|
|
1074
|
+
await server.connect(transport);
|
|
1075
|
+
console.error("lcontext MCP server running on stdio");
|
|
1076
|
+
console.error(`Connected to: ${API_BASE_URL}`);
|
|
1077
|
+
}
|
|
1078
|
+
// Don't start server if running --update
|
|
1079
|
+
if (!args.includes('--update')) {
|
|
1080
|
+
main().catch(err => {
|
|
1081
|
+
console.error("Fatal error:", err);
|
|
1082
|
+
process.exit(1);
|
|
1083
|
+
});
|
|
1084
|
+
}
|