@sealmetrics/mcp 0.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.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SealMetrics MCP Server
4
+ *
5
+ * A Model Context Protocol server that provides access to SealMetrics analytics data.
6
+ * Allows AI assistants to query traffic, conversions, sales, and generate tracking pixels.
7
+ */
8
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,809 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SealMetrics MCP Server
4
+ *
5
+ * A Model Context Protocol server that provides access to SealMetrics analytics data.
6
+ * Allows AI assistants to query traffic, conversions, sales, and generate tracking pixels.
7
+ */
8
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
11
+ // SealMetrics API Configuration
12
+ const API_BASE_URL = "https://app.sealmetrics.com/api";
13
+ const tokenCache = { token: null, expiresAt: null };
14
+ // Get credentials from environment
15
+ const API_TOKEN = process.env.SEALMETRICS_API_TOKEN;
16
+ const EMAIL = process.env.SEALMETRICS_EMAIL;
17
+ const PASSWORD = process.env.SEALMETRICS_PASSWORD;
18
+ const DEFAULT_ACCOUNT_ID = process.env.SEALMETRICS_ACCOUNT_ID;
19
+ /**
20
+ * Get authentication token
21
+ */
22
+ async function getToken() {
23
+ // If we have a direct API token, use it
24
+ if (API_TOKEN) {
25
+ return API_TOKEN;
26
+ }
27
+ // Check cached token
28
+ if (tokenCache.token && tokenCache.expiresAt && new Date() < tokenCache.expiresAt) {
29
+ return tokenCache.token;
30
+ }
31
+ // Login with email/password
32
+ if (!EMAIL || !PASSWORD) {
33
+ throw new Error("Missing credentials. Set SEALMETRICS_API_TOKEN or SEALMETRICS_EMAIL/PASSWORD");
34
+ }
35
+ const response = await fetch(`${API_BASE_URL}/auth/login`, {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/json" },
38
+ body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
39
+ });
40
+ if (!response.ok) {
41
+ throw new Error(`Authentication failed: ${response.status}`);
42
+ }
43
+ const data = await response.json();
44
+ tokenCache.token = data.access_token;
45
+ tokenCache.expiresAt = new Date(data.expires_at);
46
+ return tokenCache.token;
47
+ }
48
+ /**
49
+ * Make authenticated API request
50
+ */
51
+ async function makeRequest(endpoint, params) {
52
+ const token = await getToken();
53
+ const url = new URL(`${API_BASE_URL}${endpoint}`);
54
+ Object.entries(params).forEach(([key, value]) => {
55
+ if (value !== undefined && value !== null) {
56
+ url.searchParams.append(key, String(value));
57
+ }
58
+ });
59
+ const response = await fetch(url.toString(), {
60
+ headers: {
61
+ Authorization: `Bearer ${token}`,
62
+ Accept: "application/json",
63
+ },
64
+ });
65
+ if (!response.ok) {
66
+ const text = await response.text();
67
+ throw new Error(`API request failed: ${response.status} - ${text}`);
68
+ }
69
+ return response.json();
70
+ }
71
+ /**
72
+ * Validate date range format
73
+ */
74
+ function validateDateRange(dateRange) {
75
+ const validRanges = new Set([
76
+ "today",
77
+ "yesterday",
78
+ "last_7_days",
79
+ "last_14_days",
80
+ "last_30_days",
81
+ "last_week",
82
+ "last_month",
83
+ "this_month",
84
+ "this_year",
85
+ "last_year",
86
+ ]);
87
+ if (validRanges.has(dateRange))
88
+ return true;
89
+ if (dateRange.includes(",")) {
90
+ const parts = dateRange.split(",");
91
+ if (parts.length !== 2)
92
+ return false;
93
+ return parts.every((p) => /^\d{8}$/.test(p));
94
+ }
95
+ return false;
96
+ }
97
+ /**
98
+ * Get account ID from arguments or environment
99
+ */
100
+ function getAccountId(args) {
101
+ const provided = args.account_id;
102
+ if (provided && provided.length >= 20)
103
+ return provided;
104
+ return DEFAULT_ACCOUNT_ID || null;
105
+ }
106
+ /**
107
+ * Generate conversion pixel HTML
108
+ */
109
+ function generatePixel(accountId, eventType, label, value, ignorePageview) {
110
+ const configLines = [
111
+ ` oSm.account = "${accountId}";`,
112
+ ` oSm.event = "${eventType}";`,
113
+ ];
114
+ if (label)
115
+ configLines.push(` oSm.label = "${label}";`);
116
+ if (value !== undefined)
117
+ configLines.push(` oSm.value = ${value};`);
118
+ if (ignorePageview)
119
+ configLines.push(` oSm.ignore_pageview = 1;`);
120
+ return `<script>
121
+ /* SealMetrics Tracker Code */
122
+ var oSm = window.oSm || {};
123
+ ${configLines.join("\n")}
124
+
125
+ !(function (e) {
126
+ var t = "//app.sealmetrics.com/tag/tracker";
127
+ window.oSm = oSm;
128
+ if (window.smTrackerLoaded) sm.tracker.track(e.event);
129
+ else
130
+ Promise.all([
131
+ new Promise(function (e) {
132
+ var n = document.createElement("script");
133
+ n.src = t;
134
+ n.async = !0;
135
+ n.onload = function () {
136
+ e(t);
137
+ };
138
+ document.getElementsByTagName("head")[0].appendChild(n);
139
+ }),
140
+ ]).then(function () {
141
+ sm.tracker.track(e.event);
142
+ });
143
+ })(oSm);
144
+ </script>`;
145
+ }
146
+ /**
147
+ * Format acquisition data summary
148
+ */
149
+ function formatAcquisitionSummary(data) {
150
+ if (!data.length) {
151
+ return "## Traffic Summary\n\nNo acquisition data found for this period.";
152
+ }
153
+ const totalClicks = data.reduce((sum, item) => sum + (item.clicks || 0), 0);
154
+ const totalConversions = data.reduce((sum, item) => sum + (item.conversions || 0), 0);
155
+ const totalRevenue = data.reduce((sum, item) => sum + (item.revenue || 0), 0);
156
+ let summary = `## Traffic Summary\n\n`;
157
+ summary += `| Metric | Value |\n|--------|-------|\n`;
158
+ summary += `| Total Clicks | ${totalClicks.toLocaleString()} |\n`;
159
+ summary += `| Total Conversions | ${totalConversions.toLocaleString()} |\n`;
160
+ summary += `| Total Revenue | $${totalRevenue.toLocaleString(undefined, { minimumFractionDigits: 2 })} |\n`;
161
+ if (totalClicks > 0) {
162
+ const convRate = ((totalConversions / totalClicks) * 100).toFixed(2);
163
+ summary += `| Conversion Rate | ${convRate}% |\n`;
164
+ }
165
+ summary += `\n### Top Sources\n\n`;
166
+ const sorted = [...data].sort((a, b) => (b.clicks || 0) - (a.clicks || 0));
167
+ const top10 = sorted.slice(0, 10);
168
+ summary += `| Source | Clicks | Conversions | Revenue |\n`;
169
+ summary += `|--------|--------|-------------|----------|\n`;
170
+ for (const item of top10) {
171
+ const source = item.name || item.utm_source || "Unknown";
172
+ summary += `| ${source} | ${(item.clicks || 0).toLocaleString()} | ${(item.conversions || 0).toLocaleString()} | $${(item.revenue || 0).toLocaleString(undefined, { minimumFractionDigits: 2 })} |\n`;
173
+ }
174
+ return summary;
175
+ }
176
+ /**
177
+ * Format conversions summary
178
+ */
179
+ function formatConversionsSummary(data) {
180
+ if (!data.length) {
181
+ return "## Conversions Summary\n\nNo conversions found for this period.";
182
+ }
183
+ const totalConversions = data.length;
184
+ const totalRevenue = data.reduce((sum, item) => sum + (item.amount || 0), 0);
185
+ let summary = `## Conversions Summary\n\n`;
186
+ summary += `| Metric | Value |\n|--------|-------|\n`;
187
+ summary += `| Total Conversions | ${totalConversions.toLocaleString()} |\n`;
188
+ summary += `| Total Revenue | $${totalRevenue.toLocaleString(undefined, { minimumFractionDigits: 2 })} |\n`;
189
+ if (totalConversions > 0) {
190
+ const avgOrderValue = totalRevenue / totalConversions;
191
+ summary += `| Average Order Value | $${avgOrderValue.toLocaleString(undefined, { minimumFractionDigits: 2 })} |\n`;
192
+ }
193
+ // Group by source
194
+ const bySource = {};
195
+ for (const item of data) {
196
+ const source = item.utm_source || "Direct";
197
+ if (!bySource[source])
198
+ bySource[source] = { count: 0, revenue: 0 };
199
+ bySource[source].count++;
200
+ bySource[source].revenue += item.amount || 0;
201
+ }
202
+ summary += `\n### By Source\n\n`;
203
+ summary += `| Source | Conversions | Revenue |\n`;
204
+ summary += `|--------|-------------|----------|\n`;
205
+ const sortedSources = Object.entries(bySource).sort((a, b) => b[1].revenue - a[1].revenue);
206
+ for (const [source, stats] of sortedSources.slice(0, 10)) {
207
+ summary += `| ${source} | ${stats.count.toLocaleString()} | $${stats.revenue.toLocaleString(undefined, { minimumFractionDigits: 2 })} |\n`;
208
+ }
209
+ return summary;
210
+ }
211
+ /**
212
+ * Format microconversions summary
213
+ */
214
+ function formatMicroconversionsSummary(data) {
215
+ if (!data.length) {
216
+ return "## Microconversions Summary\n\nNo microconversions found for this period.";
217
+ }
218
+ // Group by label
219
+ const byLabel = {};
220
+ for (const item of data) {
221
+ const label = item.label || "unknown";
222
+ byLabel[label] = (byLabel[label] || 0) + 1;
223
+ }
224
+ let summary = `## Microconversions Summary\n\n`;
225
+ summary += `| Metric | Value |\n|--------|-------|\n`;
226
+ summary += `| Total Events | ${data.length.toLocaleString()} |\n`;
227
+ summary += `| Unique Event Types | ${Object.keys(byLabel).length} |\n`;
228
+ summary += `\n### By Event Type\n\n`;
229
+ summary += `| Event | Count | Percentage |\n`;
230
+ summary += `|-------|-------|------------|\n`;
231
+ const sortedLabels = Object.entries(byLabel).sort((a, b) => b[1] - a[1]);
232
+ for (const [label, count] of sortedLabels) {
233
+ const pct = ((count / data.length) * 100).toFixed(1);
234
+ summary += `| ${label} | ${count.toLocaleString()} | ${pct}% |\n`;
235
+ }
236
+ return summary;
237
+ }
238
+ // Define tools
239
+ const tools = [
240
+ {
241
+ name: "list_accounts",
242
+ description: "Get list of SealMetrics accounts available to the authenticated user",
243
+ inputSchema: {
244
+ type: "object",
245
+ properties: {},
246
+ required: [],
247
+ },
248
+ },
249
+ {
250
+ name: "get_traffic",
251
+ description: "Get traffic/acquisition data from SealMetrics. Answers questions like 'How much traffic from SEO yesterday?' or 'Show me Google Ads performance this month'",
252
+ inputSchema: {
253
+ type: "object",
254
+ properties: {
255
+ account_id: {
256
+ type: "string",
257
+ description: "SealMetrics account ID (optional if SEALMETRICS_ACCOUNT_ID is set)",
258
+ },
259
+ date_range: {
260
+ type: "string",
261
+ description: "Date range: 'yesterday', 'today', 'last_7_days', 'last_30_days', 'this_month', 'last_month', or 'YYYYMMDD,YYYYMMDD'",
262
+ },
263
+ report_type: {
264
+ type: "string",
265
+ description: "Report grouping: 'Source', 'Medium', 'Campaign', 'Term'",
266
+ default: "Source",
267
+ },
268
+ utm_source: {
269
+ type: "string",
270
+ description: "Filter by specific source (e.g., 'google', 'facebook', 'seo')",
271
+ },
272
+ utm_medium: {
273
+ type: "string",
274
+ description: "Filter by medium (e.g., 'organic', 'cpc', 'email')",
275
+ },
276
+ utm_campaign: {
277
+ type: "string",
278
+ description: "Filter by campaign name",
279
+ },
280
+ country: {
281
+ type: "string",
282
+ description: "Filter by country code (e.g., 'us', 'es')",
283
+ },
284
+ limit: {
285
+ type: "integer",
286
+ description: "Maximum number of results to return (default: 100, max: 1000)",
287
+ default: 100,
288
+ },
289
+ skip: {
290
+ type: "integer",
291
+ description: "Number of results to skip for pagination",
292
+ default: 0,
293
+ },
294
+ },
295
+ required: ["date_range"],
296
+ },
297
+ },
298
+ {
299
+ name: "get_conversions",
300
+ description: "Get conversion/sales data from SealMetrics. Answers questions like 'How many sales this month?' or 'Show conversions from Google Ads yesterday'",
301
+ inputSchema: {
302
+ type: "object",
303
+ properties: {
304
+ account_id: {
305
+ type: "string",
306
+ description: "SealMetrics account ID (optional if SEALMETRICS_ACCOUNT_ID is set)",
307
+ },
308
+ date_range: {
309
+ type: "string",
310
+ description: "Date range",
311
+ },
312
+ utm_source: {
313
+ type: "string",
314
+ description: "Filter by specific source",
315
+ },
316
+ utm_medium: {
317
+ type: "string",
318
+ description: "Filter by medium",
319
+ },
320
+ utm_campaign: {
321
+ type: "string",
322
+ description: "Filter by campaign name",
323
+ },
324
+ country: {
325
+ type: "string",
326
+ description: "Filter by country code",
327
+ },
328
+ limit: {
329
+ type: "integer",
330
+ description: "Maximum number of results",
331
+ default: 100,
332
+ },
333
+ skip: {
334
+ type: "integer",
335
+ description: "Number of results to skip",
336
+ default: 0,
337
+ },
338
+ },
339
+ required: ["date_range"],
340
+ },
341
+ },
342
+ {
343
+ name: "get_microconversions",
344
+ description: "Get microconversion data (add-to-cart, signups, etc.) from SealMetrics",
345
+ inputSchema: {
346
+ type: "object",
347
+ properties: {
348
+ account_id: {
349
+ type: "string",
350
+ description: "SealMetrics account ID",
351
+ },
352
+ date_range: {
353
+ type: "string",
354
+ description: "Date range",
355
+ },
356
+ label: {
357
+ type: "string",
358
+ description: "Filter by microconversion label",
359
+ },
360
+ utm_source: {
361
+ type: "string",
362
+ description: "Filter by source",
363
+ },
364
+ utm_medium: {
365
+ type: "string",
366
+ description: "Filter by medium",
367
+ },
368
+ country: {
369
+ type: "string",
370
+ description: "Filter by country code",
371
+ },
372
+ limit: {
373
+ type: "integer",
374
+ description: "Maximum number of results",
375
+ default: 100,
376
+ },
377
+ skip: {
378
+ type: "integer",
379
+ description: "Number of results to skip",
380
+ default: 0,
381
+ },
382
+ },
383
+ required: ["date_range"],
384
+ },
385
+ },
386
+ {
387
+ name: "get_funnel",
388
+ description: "Get funnel analysis showing progression through conversion stages",
389
+ inputSchema: {
390
+ type: "object",
391
+ properties: {
392
+ account_id: {
393
+ type: "string",
394
+ description: "SealMetrics account ID",
395
+ },
396
+ date_range: {
397
+ type: "string",
398
+ description: "Date range",
399
+ },
400
+ report_type: {
401
+ type: "string",
402
+ description: "Report grouping: 'Source', 'Medium', 'Campaign'",
403
+ default: "Source",
404
+ },
405
+ },
406
+ required: ["date_range"],
407
+ },
408
+ },
409
+ {
410
+ name: "get_roas_evolution",
411
+ description: "Get ROAS (Return on Ad Spend) evolution over time",
412
+ inputSchema: {
413
+ type: "object",
414
+ properties: {
415
+ account_id: {
416
+ type: "string",
417
+ description: "SealMetrics account ID",
418
+ },
419
+ date_range: {
420
+ type: "string",
421
+ description: "Date range",
422
+ },
423
+ time_unit: {
424
+ type: "string",
425
+ description: "Time grouping: 'daily', 'weekly', 'monthly'",
426
+ default: "daily",
427
+ },
428
+ utm_source: {
429
+ type: "string",
430
+ description: "Filter by source",
431
+ },
432
+ utm_medium: {
433
+ type: "string",
434
+ description: "Filter by medium",
435
+ },
436
+ },
437
+ required: ["date_range"],
438
+ },
439
+ },
440
+ {
441
+ name: "get_pages",
442
+ description: "Get page performance metrics including views and entry pages",
443
+ inputSchema: {
444
+ type: "object",
445
+ properties: {
446
+ account_id: {
447
+ type: "string",
448
+ description: "SealMetrics account ID",
449
+ },
450
+ date_range: {
451
+ type: "string",
452
+ description: "Date range",
453
+ },
454
+ content_grouping: {
455
+ type: "string",
456
+ description: "Filter by content group name",
457
+ },
458
+ utm_source: {
459
+ type: "string",
460
+ description: "Filter by traffic source",
461
+ },
462
+ utm_medium: {
463
+ type: "string",
464
+ description: "Filter by medium",
465
+ },
466
+ country: {
467
+ type: "string",
468
+ description: "Filter by country code",
469
+ },
470
+ show_utms: {
471
+ type: "boolean",
472
+ description: "Include UTM breakdown in results",
473
+ default: false,
474
+ },
475
+ limit: {
476
+ type: "integer",
477
+ description: "Maximum number of results",
478
+ default: 100,
479
+ },
480
+ skip: {
481
+ type: "integer",
482
+ description: "Number of results to skip",
483
+ default: 0,
484
+ },
485
+ },
486
+ required: ["date_range"],
487
+ },
488
+ },
489
+ {
490
+ name: "generate_pixel",
491
+ description: "Generate a SealMetrics tracking pixel for conversions or microconversions, ready for Google Tag Manager",
492
+ inputSchema: {
493
+ type: "object",
494
+ properties: {
495
+ account_id: {
496
+ type: "string",
497
+ description: "Your SealMetrics account ID",
498
+ },
499
+ event_type: {
500
+ type: "string",
501
+ description: "Event type: 'conversion' or 'microconversion'",
502
+ enum: ["conversion", "microconversion"],
503
+ default: "conversion",
504
+ },
505
+ label: {
506
+ type: "string",
507
+ description: "Event label (e.g., 'sales', 'add-to-cart', 'newsletter-signup')",
508
+ },
509
+ value: {
510
+ type: "number",
511
+ description: "Monetary value for the event",
512
+ },
513
+ ignore_pageview: {
514
+ type: "boolean",
515
+ description: "Set to true to avoid counting an additional pageview",
516
+ default: false,
517
+ },
518
+ },
519
+ required: [],
520
+ },
521
+ },
522
+ ];
523
+ // Initialize server
524
+ const server = new Server({
525
+ name: "sealmetrics",
526
+ version: "0.1.0",
527
+ }, {
528
+ capabilities: {
529
+ tools: {},
530
+ },
531
+ });
532
+ // Handle list tools request
533
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
534
+ tools,
535
+ }));
536
+ // Handle tool calls
537
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
538
+ const { name, arguments: args } = request.params;
539
+ try {
540
+ switch (name) {
541
+ case "list_accounts": {
542
+ const result = await makeRequest("/auth/accounts", {});
543
+ const accounts = result.data || {};
544
+ let text = "## Available SealMetrics Accounts\n\n";
545
+ if (!Object.keys(accounts).length && DEFAULT_ACCOUNT_ID) {
546
+ text += `**Default Account**\n- ID: \`${DEFAULT_ACCOUNT_ID}\`\n`;
547
+ }
548
+ else {
549
+ for (const [id, accountName] of Object.entries(accounts)) {
550
+ text += `**${accountName}**\n- ID: \`${id}\`\n\n`;
551
+ }
552
+ }
553
+ return { content: [{ type: "text", text }] };
554
+ }
555
+ case "get_traffic": {
556
+ const accountId = getAccountId(args);
557
+ if (!accountId) {
558
+ return {
559
+ content: [
560
+ {
561
+ type: "text",
562
+ text: "Error: No account_id provided and SEALMETRICS_ACCOUNT_ID not set.",
563
+ },
564
+ ],
565
+ };
566
+ }
567
+ const dateRange = args.date_range;
568
+ if (!validateDateRange(dateRange)) {
569
+ return {
570
+ content: [{ type: "text", text: `Error: Invalid date range: ${dateRange}` }],
571
+ };
572
+ }
573
+ const result = await makeRequest("/report/acquisition", {
574
+ account_id: accountId,
575
+ date_range: dateRange,
576
+ report_type: args.report_type || "Source",
577
+ utm_source: args.utm_source,
578
+ utm_medium: args.utm_medium,
579
+ utm_campaign: args.utm_campaign,
580
+ country: args.country,
581
+ limit: args.limit || 100,
582
+ skip: args.skip || 0,
583
+ });
584
+ const text = formatAcquisitionSummary(result.data || []);
585
+ return { content: [{ type: "text", text }] };
586
+ }
587
+ case "get_conversions": {
588
+ const accountId = getAccountId(args);
589
+ if (!accountId) {
590
+ return {
591
+ content: [
592
+ {
593
+ type: "text",
594
+ text: "Error: No account_id provided and SEALMETRICS_ACCOUNT_ID not set.",
595
+ },
596
+ ],
597
+ };
598
+ }
599
+ const dateRange = args.date_range;
600
+ if (!validateDateRange(dateRange)) {
601
+ return {
602
+ content: [{ type: "text", text: `Error: Invalid date range: ${dateRange}` }],
603
+ };
604
+ }
605
+ const result = await makeRequest("/report/conversions", {
606
+ account_id: accountId,
607
+ date_range: dateRange,
608
+ utm_source: args.utm_source,
609
+ utm_medium: args.utm_medium,
610
+ utm_campaign: args.utm_campaign,
611
+ country: args.country,
612
+ limit: args.limit || 100,
613
+ skip: args.skip || 0,
614
+ });
615
+ const text = formatConversionsSummary(result.data || []);
616
+ return { content: [{ type: "text", text }] };
617
+ }
618
+ case "get_microconversions": {
619
+ const accountId = getAccountId(args);
620
+ if (!accountId) {
621
+ return {
622
+ content: [
623
+ {
624
+ type: "text",
625
+ text: "Error: No account_id provided and SEALMETRICS_ACCOUNT_ID not set.",
626
+ },
627
+ ],
628
+ };
629
+ }
630
+ const dateRange = args.date_range;
631
+ if (!validateDateRange(dateRange)) {
632
+ return {
633
+ content: [{ type: "text", text: `Error: Invalid date range: ${dateRange}` }],
634
+ };
635
+ }
636
+ const result = await makeRequest("/report/microconversions", {
637
+ account_id: accountId,
638
+ date_range: dateRange,
639
+ utm_source: args.utm_source,
640
+ utm_medium: args.utm_medium,
641
+ country: args.country,
642
+ limit: args.limit || 100,
643
+ skip: args.skip || 0,
644
+ });
645
+ let data = result.data || [];
646
+ // Filter by label if specified
647
+ const labelFilter = args.label;
648
+ if (labelFilter) {
649
+ data = data.filter((item) => item.label === labelFilter);
650
+ }
651
+ const text = formatMicroconversionsSummary(data);
652
+ return { content: [{ type: "text", text }] };
653
+ }
654
+ case "get_funnel": {
655
+ const accountId = getAccountId(args);
656
+ if (!accountId) {
657
+ return {
658
+ content: [
659
+ {
660
+ type: "text",
661
+ text: "Error: No account_id provided and SEALMETRICS_ACCOUNT_ID not set.",
662
+ },
663
+ ],
664
+ };
665
+ }
666
+ const dateRange = args.date_range;
667
+ if (!validateDateRange(dateRange)) {
668
+ return {
669
+ content: [{ type: "text", text: `Error: Invalid date range: ${dateRange}` }],
670
+ };
671
+ }
672
+ const result = await makeRequest("/report/funnel", {
673
+ account_id: accountId,
674
+ date_range: dateRange,
675
+ report_type: args.report_type || "Source",
676
+ });
677
+ let text = "## Funnel Analysis\n\n";
678
+ for (const item of result.data || []) {
679
+ const source = item.name || item.utm_source || "Unknown";
680
+ text += `### ${source}\n\n`;
681
+ for (const [key, value] of Object.entries(item)) {
682
+ if (!["name", "utm_source", "_id"].includes(key)) {
683
+ text += `- **${key}:** ${value.toLocaleString()}\n`;
684
+ }
685
+ }
686
+ text += "\n";
687
+ }
688
+ return { content: [{ type: "text", text }] };
689
+ }
690
+ case "get_roas_evolution": {
691
+ const accountId = getAccountId(args);
692
+ if (!accountId) {
693
+ return {
694
+ content: [
695
+ {
696
+ type: "text",
697
+ text: "Error: No account_id provided and SEALMETRICS_ACCOUNT_ID not set.",
698
+ },
699
+ ],
700
+ };
701
+ }
702
+ const dateRange = args.date_range;
703
+ if (!validateDateRange(dateRange)) {
704
+ return {
705
+ content: [{ type: "text", text: `Error: Invalid date range: ${dateRange}` }],
706
+ };
707
+ }
708
+ const result = await makeRequest("/report/roas-evolution", {
709
+ account_id: accountId,
710
+ date_range: dateRange,
711
+ time_unit: args.time_unit || "daily",
712
+ utm_source: args.utm_source,
713
+ utm_medium: args.utm_medium,
714
+ });
715
+ let text = "## ROAS Evolution\n\n";
716
+ text += `| Date | Clicks | Page Views | Conversions | Revenue |\n`;
717
+ text += `|------|--------|------------|-------------|----------|\n`;
718
+ for (const item of result.data || []) {
719
+ const date = item._id;
720
+ text += `| ${date} | ${(item.clicks || 0).toLocaleString()} | ${(item.page_views || 0).toLocaleString()} | ${(item.conversions || 0).toLocaleString()} | $${(item.revenue || 0).toLocaleString(undefined, { minimumFractionDigits: 2 })} |\n`;
721
+ }
722
+ return { content: [{ type: "text", text }] };
723
+ }
724
+ case "get_pages": {
725
+ const accountId = getAccountId(args);
726
+ if (!accountId) {
727
+ return {
728
+ content: [
729
+ {
730
+ type: "text",
731
+ text: "Error: No account_id provided and SEALMETRICS_ACCOUNT_ID not set.",
732
+ },
733
+ ],
734
+ };
735
+ }
736
+ const dateRange = args.date_range;
737
+ if (!validateDateRange(dateRange)) {
738
+ return {
739
+ content: [{ type: "text", text: `Error: Invalid date range: ${dateRange}` }],
740
+ };
741
+ }
742
+ const result = await makeRequest("/report/pages", {
743
+ account_id: accountId,
744
+ date_range: dateRange,
745
+ content_grouping: args.content_grouping,
746
+ utm_source: args.utm_source,
747
+ utm_medium: args.utm_medium,
748
+ country: args.country,
749
+ show_utms: args.show_utms || false,
750
+ limit: args.limit || 100,
751
+ skip: args.skip || 0,
752
+ });
753
+ let text = "## Page Performance\n\n";
754
+ text += `| URL | Views | Entry Pages |\n`;
755
+ text += `|-----|-------|-------------|\n`;
756
+ for (const item of (result.data || []).slice(0, 20)) {
757
+ const url = item.url || "Unknown";
758
+ text += `| ${url} | ${(item.views || 0).toLocaleString()} | ${(item.entry_page || 0).toLocaleString()} |\n`;
759
+ }
760
+ return { content: [{ type: "text", text }] };
761
+ }
762
+ case "generate_pixel": {
763
+ const accountId = getAccountId(args);
764
+ if (!accountId) {
765
+ return {
766
+ content: [
767
+ {
768
+ type: "text",
769
+ text: "Error: No account_id provided and SEALMETRICS_ACCOUNT_ID not set.",
770
+ },
771
+ ],
772
+ };
773
+ }
774
+ const pixel = generatePixel(accountId, args.event_type || "conversion", args.label, args.value, args.ignore_pageview);
775
+ let text = "## SealMetrics Tracking Pixel\n\n";
776
+ text += "Copy this code and paste it into Google Tag Manager or your website:\n\n";
777
+ text += "```html\n" + pixel + "\n```\n\n";
778
+ text += "### Usage Instructions:\n\n";
779
+ text += "1. **For Google Tag Manager:** Create a new Custom HTML tag and paste this code\n";
780
+ text += "2. **For Direct Website Integration:** Paste this code where you want the conversion to be tracked\n";
781
+ text += "3. **Trigger:** Configure when this pixel should fire\n";
782
+ return { content: [{ type: "text", text }] };
783
+ }
784
+ default:
785
+ return {
786
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
787
+ };
788
+ }
789
+ }
790
+ catch (error) {
791
+ const message = error instanceof Error ? error.message : String(error);
792
+ return {
793
+ content: [{ type: "text", text: `Error: ${message}` }],
794
+ };
795
+ }
796
+ });
797
+ // Main entry point
798
+ async function main() {
799
+ if (!API_TOKEN && (!EMAIL || !PASSWORD)) {
800
+ console.error("Missing credentials. Set SEALMETRICS_API_TOKEN or SEALMETRICS_EMAIL/PASSWORD");
801
+ process.exit(1);
802
+ }
803
+ const transport = new StdioServerTransport();
804
+ await server.connect(transport);
805
+ }
806
+ main().catch((error) => {
807
+ console.error("Fatal error:", error);
808
+ process.exit(1);
809
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@sealmetrics/mcp",
3
+ "version": "0.1.0",
4
+ "description": "SealMetrics MCP Server - Access your analytics data from Claude and other AI assistants",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "sealmetrics-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "sealmetrics",
19
+ "analytics",
20
+ "claude",
21
+ "ai",
22
+ "model-context-protocol"
23
+ ],
24
+ "author": "SealMetrics",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.0.0",
31
+ "typescript": "^5.0.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/sealmetrics/mcp-server"
42
+ }
43
+ }