@sealmetrics/mcp 0.1.0 → 1.2.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.
Files changed (94) hide show
  1. package/README.md +149 -0
  2. package/dist/client.d.ts +45 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +136 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/errors.d.ts +10 -0
  7. package/dist/errors.d.ts.map +1 -0
  8. package/dist/errors.js +55 -0
  9. package/dist/errors.js.map +1 -0
  10. package/dist/index.d.ts +1 -6
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +122 -781
  13. package/dist/index.js.map +1 -0
  14. package/dist/resources/tracking-guide.d.ts +13 -0
  15. package/dist/resources/tracking-guide.d.ts.map +1 -0
  16. package/dist/resources/tracking-guide.js +479 -0
  17. package/dist/resources/tracking-guide.js.map +1 -0
  18. package/dist/tools/alerts.d.ts +5 -0
  19. package/dist/tools/alerts.d.ts.map +1 -0
  20. package/dist/tools/alerts.js +80 -0
  21. package/dist/tools/alerts.js.map +1 -0
  22. package/dist/tools/audience.d.ts +7 -0
  23. package/dist/tools/audience.d.ts.map +1 -0
  24. package/dist/tools/audience.js +146 -0
  25. package/dist/tools/audience.js.map +1 -0
  26. package/dist/tools/bots.d.ts +4 -0
  27. package/dist/tools/bots.d.ts.map +1 -0
  28. package/dist/tools/bots.js +52 -0
  29. package/dist/tools/bots.js.map +1 -0
  30. package/dist/tools/channels.d.ts +5 -0
  31. package/dist/tools/channels.d.ts.map +1 -0
  32. package/dist/tools/channels.js +88 -0
  33. package/dist/tools/channels.js.map +1 -0
  34. package/dist/tools/content.d.ts +3 -0
  35. package/dist/tools/content.d.ts.map +1 -0
  36. package/dist/tools/content.js +47 -0
  37. package/dist/tools/content.js.map +1 -0
  38. package/dist/tools/conversions.d.ts +6 -0
  39. package/dist/tools/conversions.d.ts.map +1 -0
  40. package/dist/tools/conversions.js +178 -0
  41. package/dist/tools/conversions.js.map +1 -0
  42. package/dist/tools/funnel.d.ts +3 -0
  43. package/dist/tools/funnel.d.ts.map +1 -0
  44. package/dist/tools/funnel.js +27 -0
  45. package/dist/tools/funnel.js.map +1 -0
  46. package/dist/tools/index.d.ts +16 -0
  47. package/dist/tools/index.d.ts.map +1 -0
  48. package/dist/tools/index.js +79 -0
  49. package/dist/tools/index.js.map +1 -0
  50. package/dist/tools/manage.d.ts +3 -0
  51. package/dist/tools/manage.d.ts.map +1 -0
  52. package/dist/tools/manage.js +55 -0
  53. package/dist/tools/manage.js.map +1 -0
  54. package/dist/tools/overview.d.ts +3 -0
  55. package/dist/tools/overview.d.ts.map +1 -0
  56. package/dist/tools/overview.js +26 -0
  57. package/dist/tools/overview.js.map +1 -0
  58. package/dist/tools/pages.d.ts +7 -0
  59. package/dist/tools/pages.d.ts.map +1 -0
  60. package/dist/tools/pages.js +207 -0
  61. package/dist/tools/pages.js.map +1 -0
  62. package/dist/tools/properties.d.ts +5 -0
  63. package/dist/tools/properties.d.ts.map +1 -0
  64. package/dist/tools/properties.js +107 -0
  65. package/dist/tools/properties.js.map +1 -0
  66. package/dist/tools/segments.d.ts +4 -0
  67. package/dist/tools/segments.d.ts.map +1 -0
  68. package/dist/tools/segments.js +49 -0
  69. package/dist/tools/segments.js.map +1 -0
  70. package/dist/tools/shared.d.ts +45 -0
  71. package/dist/tools/shared.d.ts.map +1 -0
  72. package/dist/tools/shared.js +139 -0
  73. package/dist/tools/shared.js.map +1 -0
  74. package/dist/tools/sites.d.ts +4 -0
  75. package/dist/tools/sites.d.ts.map +1 -0
  76. package/dist/tools/sites.js +36 -0
  77. package/dist/tools/sites.js.map +1 -0
  78. package/dist/tools/tracking.d.ts +3 -0
  79. package/dist/tools/tracking.d.ts.map +1 -0
  80. package/dist/tools/tracking.js +220 -0
  81. package/dist/tools/tracking.js.map +1 -0
  82. package/dist/tools/traffic.d.ts +10 -0
  83. package/dist/tools/traffic.d.ts.map +1 -0
  84. package/dist/tools/traffic.js +273 -0
  85. package/dist/tools/traffic.js.map +1 -0
  86. package/dist/tools/webhooks.d.ts +5 -0
  87. package/dist/tools/webhooks.d.ts.map +1 -0
  88. package/dist/tools/webhooks.js +101 -0
  89. package/dist/tools/webhooks.js.map +1 -0
  90. package/dist/types.d.ts +118 -0
  91. package/dist/types.d.ts.map +1 -0
  92. package/dist/types.js +22 -0
  93. package/dist/types.js.map +1 -0
  94. package/package.json +35 -27
package/dist/index.js CHANGED
@@ -1,809 +1,150 @@
1
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";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
3
  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));
4
+ import { z } from "zod";
5
+ import { SealMetricsClient } from "./client.js";
6
+ import { ALL_TOOLS } from "./tools/index.js";
7
+ import { sanitizeInput } from "./tools/shared.js";
8
+ import { TRACKING_GUIDE_URI, TRACKING_GUIDE_NAME, TRACKING_GUIDE_DESCRIPTION, TRACKING_GUIDE_CONTENT, } from "./resources/tracking-guide.js";
9
+ import { createRequire } from "module";
10
+ const require = createRequire(import.meta.url);
11
+ const { version: PKG_VERSION } = require("../package.json");
12
+ const MAX_RESPONSE_LENGTH = 100_000;
13
+ const ALLOWED_HOSTS = ["my.sealmetrics.com", "pre.sealmetrics.com", "localhost"];
14
+ function validateBaseUrl(url) {
15
+ try {
16
+ const parsed = new URL(url);
17
+ if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
18
+ console.error(`Error: SEALMETRICS_BASE_URL points to untrusted host "${parsed.hostname}".\n` +
19
+ `Allowed hosts: ${ALLOWED_HOSTS.join(", ")}`);
20
+ process.exit(1);
57
21
  }
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
22
  }
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));
23
+ catch {
24
+ console.error(`Error: Invalid SEALMETRICS_BASE_URL: ${url}`);
25
+ process.exit(1);
94
26
  }
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
27
  }
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>`;
28
+ const API_KEY = process.env.SEALMETRICS_API_KEY;
29
+ const BASE_URL = process.env.SEALMETRICS_BASE_URL ?? "https://my.sealmetrics.com/api/v1";
30
+ if (process.env.SEALMETRICS_BASE_URL) {
31
+ validateBaseUrl(BASE_URL);
145
32
  }
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`;
33
+ const client = API_KEY
34
+ ? new SealMetricsClient(API_KEY, BASE_URL)
35
+ : null;
36
+ const server = new McpServer({
37
+ name: "sealmetrics",
38
+ version: PKG_VERSION,
39
+ });
40
+ function jsonSchemaToZod(prop) {
41
+ const type = prop.type;
42
+ const enumValues = prop.enum;
43
+ const description = prop.description;
44
+ let schema;
45
+ if (enumValues && enumValues.length > 0) {
46
+ schema = z.enum(enumValues);
173
47
  }
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.";
48
+ else if (type === "number" || type === "integer") {
49
+ schema = z.number();
182
50
  }
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`;
51
+ else if (type === "boolean") {
52
+ schema = z.boolean();
192
53
  }
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;
54
+ else {
55
+ schema = z.string();
201
56
  }
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`;
57
+ if (description) {
58
+ schema = schema.describe(description);
208
59
  }
209
- return summary;
60
+ return schema;
210
61
  }
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;
62
+ function safeStringify(value) {
63
+ try {
64
+ const json = JSON.stringify(value, null, 2);
65
+ if (json.length > MAX_RESPONSE_LENGTH) {
66
+ return json.slice(0, MAX_RESPONSE_LENGTH) + "\n... (truncated)";
67
+ }
68
+ return json;
223
69
  }
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`;
70
+ catch {
71
+ return '{"error": "Response could not be serialized"}';
235
72
  }
236
- return summary;
237
73
  }
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`;
74
+ // Register all tools
75
+ for (const tool of ALL_TOOLS) {
76
+ const shape = {};
77
+ for (const [key, prop] of Object.entries(tool.inputSchema.properties)) {
78
+ // All tool params are optional (site_id resolved from env, others have defaults)
79
+ shape[key] = jsonSchemaToZod(prop).optional();
80
+ }
81
+ const toolName = tool.name;
82
+ const handler = tool.handler;
83
+ server.tool(toolName, tool.description, shape, async (args) => {
84
+ if (!client) {
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: "Error: SEALMETRICS_API_KEY is not configured.\n\n" +
90
+ "To set it up:\n" +
91
+ "1. Go to Settings > API Tokens in your SealMetrics dashboard (https://my.sealmetrics.com)\n" +
92
+ "2. Generate an API key (starts with 'sm_')\n" +
93
+ "3. Set the SEALMETRICS_API_KEY environment variable in your MCP server configuration\n" +
94
+ "4. Restart the MCP server",
95
+ },
96
+ ],
97
+ isError: true,
98
+ };
99
+ }
100
+ try {
101
+ // Sanitize all string arguments to prevent oversized or malformed inputs
102
+ const sanitizedArgs = {};
103
+ for (const [key, value] of Object.entries(args)) {
104
+ if (typeof value === "string") {
105
+ sanitizedArgs[key] = sanitizeInput(value, key);
547
106
  }
548
107
  else {
549
- for (const [id, accountName] of Object.entries(accounts)) {
550
- text += `**${accountName}**\n- ID: \`${id}\`\n\n`;
551
- }
108
+ sanitizedArgs[key] = value;
552
109
  }
553
- return { content: [{ type: "text", text }] };
554
110
  }
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
- };
111
+ const result = await handler(client, sanitizedArgs);
112
+ return {
113
+ content: [
114
+ {
115
+ type: "text",
116
+ text: safeStringify(result),
117
+ },
118
+ ],
119
+ };
788
120
  }
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
121
+ catch (error) {
122
+ const message = error instanceof Error ? error.message : "Unknown error occurred";
123
+ return {
124
+ content: [{ type: "text", text: `Error: ${message}` }],
125
+ isError: true,
126
+ };
127
+ }
128
+ });
129
+ }
130
+ // Register resources
131
+ server.resource(TRACKING_GUIDE_NAME, TRACKING_GUIDE_URI, { description: TRACKING_GUIDE_DESCRIPTION, mimeType: "text/markdown" }, async () => ({
132
+ contents: [
133
+ {
134
+ uri: TRACKING_GUIDE_URI,
135
+ mimeType: "text/markdown",
136
+ text: TRACKING_GUIDE_CONTENT,
137
+ },
138
+ ],
139
+ }));
140
+ // Start the server
798
141
  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
142
  const transport = new StdioServerTransport();
804
143
  await server.connect(transport);
805
144
  }
806
145
  main().catch((error) => {
807
- console.error("Fatal error:", error);
146
+ const message = error instanceof Error ? error.message : String(error);
147
+ console.error("Fatal error:", message);
808
148
  process.exit(1);
809
149
  });
150
+ //# sourceMappingURL=index.js.map