@sealmetrics/mcp 0.1.0 → 1.3.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 (103) hide show
  1. package/README.md +173 -0
  2. package/dist/client.d.ts +68 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +225 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/embedded.d.ts +29 -0
  7. package/dist/embedded.d.ts.map +1 -0
  8. package/dist/embedded.js +37 -0
  9. package/dist/embedded.js.map +1 -0
  10. package/dist/errors.d.ts +10 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +55 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/index.d.ts +1 -6
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +3809 -780
  17. package/dist/index.js.map +1 -0
  18. package/dist/resources/tracking-guide.d.ts +13 -0
  19. package/dist/resources/tracking-guide.d.ts.map +1 -0
  20. package/dist/resources/tracking-guide.js +479 -0
  21. package/dist/resources/tracking-guide.js.map +1 -0
  22. package/dist/sealmetrics.mcpb +0 -0
  23. package/dist/server.d.ts +38 -0
  24. package/dist/server.d.ts.map +1 -0
  25. package/dist/server.js +132 -0
  26. package/dist/server.js.map +1 -0
  27. package/dist/tools/alerts.d.ts +5 -0
  28. package/dist/tools/alerts.d.ts.map +1 -0
  29. package/dist/tools/alerts.js +80 -0
  30. package/dist/tools/alerts.js.map +1 -0
  31. package/dist/tools/audience.d.ts +7 -0
  32. package/dist/tools/audience.d.ts.map +1 -0
  33. package/dist/tools/audience.js +146 -0
  34. package/dist/tools/audience.js.map +1 -0
  35. package/dist/tools/bots.d.ts +4 -0
  36. package/dist/tools/bots.d.ts.map +1 -0
  37. package/dist/tools/bots.js +52 -0
  38. package/dist/tools/bots.js.map +1 -0
  39. package/dist/tools/channels.d.ts +5 -0
  40. package/dist/tools/channels.d.ts.map +1 -0
  41. package/dist/tools/channels.js +88 -0
  42. package/dist/tools/channels.js.map +1 -0
  43. package/dist/tools/content.d.ts +3 -0
  44. package/dist/tools/content.d.ts.map +1 -0
  45. package/dist/tools/content.js +47 -0
  46. package/dist/tools/content.js.map +1 -0
  47. package/dist/tools/conversions.d.ts +9 -0
  48. package/dist/tools/conversions.d.ts.map +1 -0
  49. package/dist/tools/conversions.js +427 -0
  50. package/dist/tools/conversions.js.map +1 -0
  51. package/dist/tools/funnel.d.ts +3 -0
  52. package/dist/tools/funnel.d.ts.map +1 -0
  53. package/dist/tools/funnel.js +27 -0
  54. package/dist/tools/funnel.js.map +1 -0
  55. package/dist/tools/index.d.ts +16 -0
  56. package/dist/tools/index.d.ts.map +1 -0
  57. package/dist/tools/index.js +83 -0
  58. package/dist/tools/index.js.map +1 -0
  59. package/dist/tools/overview.d.ts +3 -0
  60. package/dist/tools/overview.d.ts.map +1 -0
  61. package/dist/tools/overview.js +26 -0
  62. package/dist/tools/overview.js.map +1 -0
  63. package/dist/tools/pages.d.ts +7 -0
  64. package/dist/tools/pages.d.ts.map +1 -0
  65. package/dist/tools/pages.js +307 -0
  66. package/dist/tools/pages.js.map +1 -0
  67. package/dist/tools/properties.d.ts +5 -0
  68. package/dist/tools/properties.d.ts.map +1 -0
  69. package/dist/tools/properties.js +107 -0
  70. package/dist/tools/properties.js.map +1 -0
  71. package/dist/tools/segments.d.ts +4 -0
  72. package/dist/tools/segments.d.ts.map +1 -0
  73. package/dist/tools/segments.js +49 -0
  74. package/dist/tools/segments.js.map +1 -0
  75. package/dist/tools/setup.d.ts +49 -0
  76. package/dist/tools/setup.d.ts.map +1 -0
  77. package/dist/tools/setup.js +347 -0
  78. package/dist/tools/setup.js.map +1 -0
  79. package/dist/tools/shared.d.ts +33 -0
  80. package/dist/tools/shared.d.ts.map +1 -0
  81. package/dist/tools/shared.js +40 -0
  82. package/dist/tools/shared.js.map +1 -0
  83. package/dist/tools/sites.d.ts +4 -0
  84. package/dist/tools/sites.d.ts.map +1 -0
  85. package/dist/tools/sites.js +36 -0
  86. package/dist/tools/sites.js.map +1 -0
  87. package/dist/tools/tracking.d.ts +3 -0
  88. package/dist/tools/tracking.d.ts.map +1 -0
  89. package/dist/tools/tracking.js +220 -0
  90. package/dist/tools/tracking.js.map +1 -0
  91. package/dist/tools/traffic.d.ts +10 -0
  92. package/dist/tools/traffic.d.ts.map +1 -0
  93. package/dist/tools/traffic.js +273 -0
  94. package/dist/tools/traffic.js.map +1 -0
  95. package/dist/tools/webhooks.d.ts +5 -0
  96. package/dist/tools/webhooks.d.ts.map +1 -0
  97. package/dist/tools/webhooks.js +101 -0
  98. package/dist/tools/webhooks.js.map +1 -0
  99. package/dist/types.d.ts +118 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +22 -0
  102. package/dist/types.js.map +1 -0
  103. package/package.json +44 -27
package/dist/index.js CHANGED
@@ -1,809 +1,3838 @@
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
+
3
+ // dist/index.js
9
4
  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`, {
5
+
6
+ // dist/server.js
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { z } from "zod";
9
+
10
+ // dist/errors.js
11
+ var SealMetricsAPIError = class extends Error {
12
+ statusCode;
13
+ userMessage;
14
+ detail;
15
+ constructor(statusCode, userMessage, detail) {
16
+ super(userMessage);
17
+ this.statusCode = statusCode;
18
+ this.userMessage = userMessage;
19
+ this.detail = detail;
20
+ this.name = "SealMetricsAPIError";
21
+ }
22
+ };
23
+ function mapHttpError(status, body, siteId) {
24
+ switch (status) {
25
+ case 401:
26
+ return new SealMetricsAPIError(401, "Invalid API key. Generate one at Settings > API Tokens in your SealMetrics dashboard.", body);
27
+ case 403:
28
+ return new SealMetricsAPIError(403, siteId ? `Access denied to site "${siteId}". Your API key may not have access to this site.` : "Access denied. Your API key does not have the required permissions.", body);
29
+ case 404:
30
+ return new SealMetricsAPIError(404, siteId ? `Site "${siteId}" not found. Use list_sites to see available sites.` : "Resource not found.", body);
31
+ case 422:
32
+ return new SealMetricsAPIError(422, `Invalid request parameters: ${extractValidationMessage(body)}`, body);
33
+ case 429:
34
+ return new SealMetricsAPIError(429, "Rate limit exceeded. Please wait a moment before retrying.", body);
35
+ default:
36
+ if (status >= 500) {
37
+ return new SealMetricsAPIError(status, "SealMetrics API is temporarily unavailable. Please try again later.", body);
38
+ }
39
+ return new SealMetricsAPIError(status, `API request failed with status ${status}.`, body);
40
+ }
41
+ }
42
+ function extractValidationMessage(body) {
43
+ try {
44
+ const parsed = JSON.parse(body);
45
+ if (parsed.detail) {
46
+ if (Array.isArray(parsed.detail)) {
47
+ return parsed.detail.map((d) => `${d.loc?.join(".") ?? "field"}: ${d.msg ?? "invalid"}`).join("; ");
48
+ }
49
+ return String(parsed.detail);
50
+ }
51
+ return body;
52
+ } catch {
53
+ return body;
54
+ }
55
+ }
56
+
57
+ // dist/client.js
58
+ var DEFAULT_TIMEOUT_MS = 3e4;
59
+ var MAX_RETRIES = 2;
60
+ var RETRY_BASE_MS = 1e3;
61
+ var SealMetricsClient = class {
62
+ /**
63
+ * Read-only api_key (X-API-Key). Optional + mutable so the server can start in
64
+ * setup-only mode without a key (RF-3202) and adopt the key returned by
65
+ * provision_site at runtime to enable the read-only tools (RF-3202b). Held in
66
+ * memory only — never echoed to the model or logs (VAL-3201).
67
+ */
68
+ apiKey;
69
+ baseUrl;
70
+ constructor(apiKey, baseUrl) {
71
+ this.apiKey = apiKey;
72
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
73
+ }
74
+ /** True once a read-only api_key is available (env at boot or post-provision). */
75
+ hasApiKey() {
76
+ return typeof this.apiKey === "string" && this.apiKey.length > 0;
77
+ }
78
+ /** Return the in-memory api_key (for setup-core verify). Never logged by callers. */
79
+ getApiKey() {
80
+ return this.apiKey;
81
+ }
82
+ /** Adopt the api_key returned by provision_site so read-only tools work (RF-3202b). */
83
+ setApiKey(apiKey) {
84
+ this.apiKey = apiKey;
85
+ }
86
+ /**
87
+ * Authenticated POST (RF-3201). The client was GET-only in v1.2.0; the
88
+ * write-path needs it. NOT auto-retried — POST is not idempotent (a blind retry
89
+ * of /provision could create duplicate accounts). Authenticates with the
90
+ * publishable provision key (`provisionKey`) for provisioning, or X-API-Key
91
+ * otherwise. On 2xx returns the parsed JSON; on non-2xx / network throws
92
+ * SealMetricsAPIError carrying the status code for the caller to map.
93
+ */
94
+ async post(path, body, opts) {
95
+ const url = `${this.baseUrl}${path}`;
96
+ const headers = {
97
+ "Content-Type": "application/json",
98
+ Accept: "application/json"
99
+ };
100
+ if (opts?.provisionKey) {
101
+ headers["X-Provision-Key"] = opts.provisionKey;
102
+ } else if (this.apiKey) {
103
+ headers["X-API-Key"] = this.apiKey;
104
+ }
105
+ const controller = new AbortController();
106
+ const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
107
+ let response;
108
+ try {
109
+ response = await fetch(url, {
36
110
  method: "POST",
37
- headers: { "Content-Type": "application/json" },
38
- body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
39
- });
111
+ headers,
112
+ body: JSON.stringify(body),
113
+ signal: controller.signal
114
+ });
115
+ } catch (error) {
116
+ if (error instanceof Error && error.name === "AbortError") {
117
+ throw new SealMetricsAPIError(0, "Request timed out after 30 seconds.");
118
+ }
119
+ throw new SealMetricsAPIError(0, error instanceof Error ? error.message : "Network error");
120
+ } finally {
121
+ clearTimeout(timeout);
122
+ }
123
+ const text = await response.text();
40
124
  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));
125
+ throw mapHttpError(response.status, text);
126
+ }
127
+ try {
128
+ return JSON.parse(text);
129
+ } catch {
130
+ throw new SealMetricsAPIError(response.status, "API returned invalid JSON.", text.slice(0, 500));
131
+ }
132
+ }
133
+ /**
134
+ * Send an authenticated GET request. Unwraps the API envelope
135
+ * and returns just the `data` field.
136
+ */
137
+ async request(path, params) {
138
+ const raw = await this.requestRaw(path, params);
139
+ return raw.data;
140
+ }
141
+ /**
142
+ * Send an authenticated GET request and return the full JSON body
143
+ * without unwrapping. Use this for endpoints that don't use the
144
+ * standard APIResponse envelope (e.g. alerts, webhooks).
145
+ */
146
+ async requestDirect(path, params) {
147
+ const raw = await this.requestRaw(path, params);
148
+ return raw;
149
+ }
150
+ /**
151
+ * Send an authenticated GET request and return the full response
152
+ * including pagination fields (total, page, has_next, etc.).
153
+ * Use this for paginated endpoints.
154
+ */
155
+ async requestPaginated(path, params) {
156
+ const raw = await this.requestRaw(path, params);
157
+ const result = {
158
+ data: raw.data,
159
+ total: raw.total ?? 0,
160
+ page: raw.page ?? 1,
161
+ page_size: raw.page_size ?? 0,
162
+ has_next: raw.has_next ?? false
163
+ };
164
+ if (raw.comparison != null)
165
+ result.comparison = raw.comparison;
166
+ if (raw.totals != null)
167
+ result.totals = raw.totals;
168
+ return result;
169
+ }
170
+ async requestRaw(path, params) {
171
+ if (!this.apiKey) {
172
+ throw new SealMetricsAPIError(401, "AUTH_REQUIRED: a SealMetrics api_key is required for data tools. Provision a site first (provision_site) or set SEALMETRICS_API_KEY.");
173
+ }
174
+ const apiKey = this.apiKey;
175
+ const url = new URL(`${this.baseUrl}${path}`);
176
+ if (params) {
177
+ for (const [key, value] of Object.entries(params)) {
178
+ if (value === void 0)
179
+ continue;
180
+ if (Array.isArray(value)) {
181
+ for (const item of value) {
182
+ if (item !== void 0 && item !== "") {
183
+ url.searchParams.append(key, item);
184
+ }
185
+ }
186
+ } else if (value !== "") {
187
+ url.searchParams.set(key, value);
188
+ }
189
+ }
190
+ }
191
+ let lastError;
192
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
193
+ if (attempt > 0) {
194
+ const delay = RETRY_BASE_MS * Math.pow(2, attempt - 1);
195
+ await sleep(delay);
196
+ }
197
+ const controller = new AbortController();
198
+ const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
199
+ try {
200
+ const response = await fetch(url.toString(), {
201
+ method: "GET",
202
+ headers: {
203
+ "X-API-Key": apiKey,
204
+ Accept: "application/json"
205
+ },
206
+ signal: controller.signal
207
+ });
208
+ if (response.ok) {
209
+ const text = await response.text();
210
+ try {
211
+ return JSON.parse(text);
212
+ } catch {
213
+ throw new SealMetricsAPIError(response.status, "API returned invalid JSON.", text.slice(0, 500));
214
+ }
57
215
  }
216
+ const body = await response.text();
217
+ if (response.status === 429 || response.status >= 500) {
218
+ if (response.status === 429) {
219
+ const retryAfter = parseRetryAfter(response.headers.get("Retry-After"));
220
+ if (retryAfter > 0) {
221
+ await sleep(retryAfter);
222
+ }
223
+ }
224
+ lastError = mapHttpError(response.status, body);
225
+ continue;
226
+ }
227
+ const siteIdRaw = params?.["site_id"] ?? params?.["account_id"] ?? void 0;
228
+ const siteId = typeof siteIdRaw === "string" ? siteIdRaw : void 0;
229
+ throw mapHttpError(response.status, body, siteId);
230
+ } catch (error) {
231
+ if (error instanceof SealMetricsAPIError) {
232
+ throw error;
233
+ }
234
+ if (error instanceof Error && error.name === "AbortError") {
235
+ lastError = new SealMetricsAPIError(0, "Request timed out after 30 seconds.");
236
+ continue;
237
+ }
238
+ lastError = error instanceof Error ? error : new Error("Unknown error occurred");
239
+ continue;
240
+ } finally {
241
+ clearTimeout(timeout);
242
+ }
243
+ }
244
+ throw lastError ?? new SealMetricsAPIError(0, "Request failed after retries.");
245
+ }
246
+ };
247
+ function sleep(ms) {
248
+ return new Promise((resolve) => setTimeout(resolve, ms));
249
+ }
250
+ function parseRetryAfter(header) {
251
+ if (!header)
252
+ return 0;
253
+ const seconds = parseInt(header, 10);
254
+ if (!isNaN(seconds) && seconds > 0 && seconds <= 120) {
255
+ return seconds * 1e3;
256
+ }
257
+ return 0;
258
+ }
259
+
260
+ // dist/types.js
261
+ var VALID_PERIODS = [
262
+ "today",
263
+ "yesterday",
264
+ "7d",
265
+ "30d",
266
+ "90d",
267
+ "12m",
268
+ "this_week",
269
+ "wtd",
270
+ "last_week",
271
+ "this_month",
272
+ "mtd",
273
+ "last_month",
274
+ "this_quarter",
275
+ "qtd",
276
+ "last_quarter",
277
+ "this_year",
278
+ "ytd",
279
+ "last_year"
280
+ ];
281
+
282
+ // dist/tools/shared.js
283
+ var PERIOD_SCHEMA = {
284
+ type: "string",
285
+ description: `Time period for the report. Examples: "today", "yesterday", "7d", "30d", "90d", "this_month", "last_month", "this_year". Default: "30d".`,
286
+ enum: [...VALID_PERIODS],
287
+ default: "30d"
288
+ };
289
+ var COMPARE_SCHEMA = {
290
+ type: "string",
291
+ description: 'Comparison mode. "previous" compares with the prior period of the same length. "yoy" compares with the same dates last year.',
292
+ enum: ["previous", "yoy"]
293
+ };
294
+ var LIMIT_SCHEMA = {
295
+ type: "number",
296
+ description: "Maximum number of rows to return (default: 20, max: 100).",
297
+ default: 20
298
+ };
299
+ var SORT_ORDER_SCHEMA = {
300
+ type: "string",
301
+ description: 'Sort direction: "asc" or "desc" (default: "desc").',
302
+ enum: ["asc", "desc"],
303
+ default: "desc"
304
+ };
305
+ var PAGE_SCHEMA = {
306
+ type: "number",
307
+ description: "Page number for paginated results (default: 1).",
308
+ default: 1
309
+ };
310
+ function resolveSiteId(args) {
311
+ const siteId = args.site_id ?? process.env.SEALMETRICS_SITE_ID;
312
+ if (!siteId) {
313
+ throw new Error("site_id is required. Either pass it as a parameter or set the SEALMETRICS_SITE_ID environment variable.");
314
+ }
315
+ return siteId;
316
+ }
317
+
318
+ // dist/tools/sites.js
319
+ var listSitesTool = {
320
+ name: "list_sites",
321
+ description: "List all sites (web properties) accessible with your API key. Returns site IDs, names, and domains. Use this to find the site_id needed for other tools.",
322
+ inputSchema: {
323
+ type: "object",
324
+ properties: {}
325
+ },
326
+ handler: async (client) => {
327
+ const data = await client.request("/sites");
328
+ return data.sites.map((s) => ({
329
+ site_id: s.id,
330
+ name: s.name,
331
+ domains: s.domains,
332
+ timezone: s.timezone
333
+ }));
334
+ }
335
+ };
336
+ var getSiteTool = {
337
+ name: "get_site",
338
+ description: "Get detailed information about a specific site: name, domains, timezone, configuration, and tracking status. Use list_sites first to find available site IDs.",
339
+ inputSchema: {
340
+ type: "object",
341
+ properties: {
342
+ site_id: {
343
+ type: "string",
344
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
345
+ }
346
+ }
347
+ },
348
+ handler: async (client, args) => {
349
+ const siteId = resolveSiteId(args);
350
+ return client.request(`/sites/${encodeURIComponent(siteId)}`);
351
+ }
352
+ };
353
+
354
+ // dist/tools/overview.js
355
+ var getOverviewTool = {
356
+ name: "get_overview",
357
+ description: "Get dashboard KPIs: pageviews, entrances, bounce rate, conversions, revenue. Includes time series data and optional period-over-period comparison. This is the best starting point for understanding a site's performance.",
358
+ inputSchema: {
359
+ type: "object",
360
+ properties: {
361
+ site_id: {
362
+ type: "string",
363
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
364
+ },
365
+ period: PERIOD_SCHEMA,
366
+ compare: COMPARE_SCHEMA
367
+ }
368
+ },
369
+ handler: async (client, args) => {
370
+ const siteId = resolveSiteId(args);
371
+ const params = {
372
+ site_id: siteId,
373
+ period: args.period ?? "30d",
374
+ compare: args.compare
375
+ };
376
+ return client.request("/stats/overview", params);
377
+ }
378
+ };
379
+
380
+ // dist/tools/traffic.js
381
+ var TRAFFIC_SORT_BY = {
382
+ type: "string",
383
+ description: "Field to sort by.",
384
+ enum: ["entrances", "engaged_entrances", "page_views", "conversions", "revenue", "bounce_rate"],
385
+ default: "entrances"
386
+ };
387
+ function trafficParams(args) {
388
+ return {
389
+ site_id: resolveSiteId(args),
390
+ period: args.period ?? "30d",
391
+ compare: args.compare,
392
+ page_size: String(args.limit ?? 20),
393
+ page: args.page != null ? String(args.page) : void 0,
394
+ sort_by: args.sort_by ?? "entrances",
395
+ sort_order: args.sort_order ?? "desc"
396
+ };
397
+ }
398
+ var getTrafficSourcesTool = {
399
+ name: "get_traffic_sources",
400
+ description: "Get traffic broken down by source (utm_source): google, facebook, twitter, direct, etc. Shows entrances, pageviews, conversions, and revenue per source.",
401
+ inputSchema: {
402
+ type: "object",
403
+ properties: {
404
+ site_id: {
405
+ type: "string",
406
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
407
+ },
408
+ period: PERIOD_SCHEMA,
409
+ compare: COMPARE_SCHEMA,
410
+ limit: LIMIT_SCHEMA,
411
+ page: PAGE_SCHEMA,
412
+ sort_by: TRAFFIC_SORT_BY,
413
+ sort_order: SORT_ORDER_SCHEMA
414
+ }
415
+ },
416
+ handler: async (client, args) => {
417
+ return client.requestPaginated("/stats/sources", trafficParams(args));
418
+ }
419
+ };
420
+ var getTrafficMediumsTool = {
421
+ name: "get_traffic_mediums",
422
+ description: "Get traffic broken down by medium (utm_medium): organic, cpc, email, referral, social, etc. Useful for understanding which marketing channels drive traffic.",
423
+ inputSchema: {
424
+ type: "object",
425
+ properties: {
426
+ site_id: {
427
+ type: "string",
428
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
429
+ },
430
+ period: PERIOD_SCHEMA,
431
+ compare: COMPARE_SCHEMA,
432
+ limit: LIMIT_SCHEMA,
433
+ page: PAGE_SCHEMA,
434
+ sort_by: TRAFFIC_SORT_BY,
435
+ sort_order: SORT_ORDER_SCHEMA
436
+ }
437
+ },
438
+ handler: async (client, args) => {
439
+ return client.requestPaginated("/stats/mediums", trafficParams(args));
440
+ }
441
+ };
442
+ var getCampaignsTool = {
443
+ name: "get_campaigns",
444
+ description: "Get traffic and conversions broken down by campaign (utm_campaign). Shows performance of individual marketing campaigns including entrances, conversions, and revenue.",
445
+ inputSchema: {
446
+ type: "object",
447
+ properties: {
448
+ site_id: {
449
+ type: "string",
450
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
451
+ },
452
+ period: PERIOD_SCHEMA,
453
+ compare: COMPARE_SCHEMA,
454
+ limit: LIMIT_SCHEMA,
455
+ page: PAGE_SCHEMA,
456
+ sort_by: TRAFFIC_SORT_BY,
457
+ sort_order: SORT_ORDER_SCHEMA,
458
+ utm_source: {
459
+ type: "string",
460
+ description: "Filter by source (e.g. 'google')."
461
+ },
462
+ utm_medium: {
463
+ type: "string",
464
+ description: "Filter by medium (e.g. 'cpc')."
465
+ }
466
+ }
467
+ },
468
+ handler: async (client, args) => {
469
+ const params = trafficParams(args);
470
+ params.utm_source = args.utm_source;
471
+ params.utm_medium = args.utm_medium;
472
+ return client.requestPaginated("/stats/campaigns", params);
473
+ }
474
+ };
475
+ var getTermsTool = {
476
+ name: "get_terms",
477
+ description: "Get traffic and conversions broken down by UTM term (keyword). Shows which search keywords or ad terms drive the most traffic. Supports filtering by source, medium, and campaign.",
478
+ inputSchema: {
479
+ type: "object",
480
+ properties: {
481
+ site_id: {
482
+ type: "string",
483
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
484
+ },
485
+ period: PERIOD_SCHEMA,
486
+ compare: COMPARE_SCHEMA,
487
+ limit: LIMIT_SCHEMA,
488
+ page: PAGE_SCHEMA,
489
+ sort_by: TRAFFIC_SORT_BY,
490
+ sort_order: SORT_ORDER_SCHEMA,
491
+ utm_source: {
492
+ type: "string",
493
+ description: "Filter by source (e.g. 'google')."
494
+ },
495
+ utm_medium: {
496
+ type: "string",
497
+ description: "Filter by medium (e.g. 'cpc')."
498
+ },
499
+ utm_campaign: {
500
+ type: "string",
501
+ description: "Filter by campaign name."
502
+ },
503
+ country: {
504
+ type: "string",
505
+ description: "Filter by country code (e.g. 'ES', 'US')."
506
+ }
507
+ }
508
+ },
509
+ handler: async (client, args) => {
510
+ const params = trafficParams(args);
511
+ params.utm_source = args.utm_source;
512
+ params.utm_medium = args.utm_medium;
513
+ params.utm_campaign = args.utm_campaign;
514
+ params.country = args.country;
515
+ return client.requestPaginated("/stats/terms", params);
516
+ }
517
+ };
518
+ function topNParams(args) {
519
+ return {
520
+ site_id: resolveSiteId(args),
521
+ period: args.period ?? "30d",
522
+ limit: String(args.limit ?? 10)
523
+ };
524
+ }
525
+ var getTopSourcesTool = {
526
+ name: "get_top_sources",
527
+ description: "Get top traffic sources ranked by entrances. Returns a compact list of the top N sources (utm_source) without pagination \u2014 ideal for quick rankings and summaries.",
528
+ inputSchema: {
529
+ type: "object",
530
+ properties: {
531
+ site_id: {
532
+ type: "string",
533
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
534
+ },
535
+ period: PERIOD_SCHEMA,
536
+ limit: LIMIT_SCHEMA,
537
+ utm_medium: {
538
+ type: "string",
539
+ description: "Filter by medium (e.g. 'cpc')."
540
+ },
541
+ country: {
542
+ type: "string",
543
+ description: "Filter by country code (e.g. 'ES', 'US')."
544
+ }
545
+ }
546
+ },
547
+ handler: async (client, args) => {
548
+ const params = topNParams(args);
549
+ params.utm_medium = args.utm_medium;
550
+ params.country = args.country;
551
+ return client.request("/stats/sources/top", params);
552
+ }
553
+ };
554
+ var getTopCampaignsTool = {
555
+ name: "get_top_campaigns",
556
+ description: "Get top campaigns ranked by entrances. Returns a compact list of the top N campaigns (utm_campaign) \u2014 ideal for quick rankings.",
557
+ inputSchema: {
558
+ type: "object",
559
+ properties: {
560
+ site_id: {
561
+ type: "string",
562
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
563
+ },
564
+ period: PERIOD_SCHEMA,
565
+ limit: LIMIT_SCHEMA,
566
+ utm_source: {
567
+ type: "string",
568
+ description: "Filter by source (e.g. 'google')."
569
+ },
570
+ utm_medium: {
571
+ type: "string",
572
+ description: "Filter by medium (e.g. 'cpc')."
573
+ },
574
+ country: {
575
+ type: "string",
576
+ description: "Filter by country code (e.g. 'ES', 'US')."
577
+ }
578
+ }
579
+ },
580
+ handler: async (client, args) => {
581
+ const params = topNParams(args);
582
+ params.utm_source = args.utm_source;
583
+ params.utm_medium = args.utm_medium;
584
+ params.country = args.country;
585
+ return client.request("/stats/campaigns/top", params);
586
+ }
587
+ };
588
+ var getTopTermsTool = {
589
+ name: "get_top_terms",
590
+ description: "Get top UTM terms (keywords) ranked by entrances. Returns a compact list of the top N terms \u2014 ideal for quick keyword rankings.",
591
+ inputSchema: {
592
+ type: "object",
593
+ properties: {
594
+ site_id: {
595
+ type: "string",
596
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
597
+ },
598
+ period: PERIOD_SCHEMA,
599
+ limit: LIMIT_SCHEMA,
600
+ utm_source: {
601
+ type: "string",
602
+ description: "Filter by source (e.g. 'google')."
603
+ },
604
+ utm_medium: {
605
+ type: "string",
606
+ description: "Filter by medium (e.g. 'cpc')."
607
+ },
608
+ utm_campaign: {
609
+ type: "string",
610
+ description: "Filter by campaign name."
611
+ },
612
+ country: {
613
+ type: "string",
614
+ description: "Filter by country code (e.g. 'ES', 'US')."
615
+ }
616
+ }
617
+ },
618
+ handler: async (client, args) => {
619
+ const params = topNParams(args);
620
+ params.utm_source = args.utm_source;
621
+ params.utm_medium = args.utm_medium;
622
+ params.utm_campaign = args.utm_campaign;
623
+ params.country = args.country;
624
+ return client.request("/stats/terms/top", params);
625
+ }
626
+ };
627
+ var getTopReferrersTool = {
628
+ name: "get_top_referrers",
629
+ description: "Get top referrer domains ranked by entrances. Shows which external sites send the most traffic.",
630
+ inputSchema: {
631
+ type: "object",
632
+ properties: {
633
+ site_id: {
634
+ type: "string",
635
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
636
+ },
637
+ period: PERIOD_SCHEMA,
638
+ limit: LIMIT_SCHEMA,
639
+ country: {
640
+ type: "string",
641
+ description: "Filter by country code (e.g. 'ES', 'US')."
642
+ }
643
+ }
644
+ },
645
+ handler: async (client, args) => {
646
+ const params = topNParams(args);
647
+ params.country = args.country;
648
+ return client.request("/stats/referrers/top", params);
649
+ }
650
+ };
651
+
652
+ // dist/tools/pages.js
653
+ var MULTI_FILTER_SCHEMA = {
654
+ country: {
655
+ type: "array",
656
+ items: { type: "string", minLength: 1 },
657
+ minItems: 1,
658
+ description: "Filter by country code(s) (e.g. ['ES', 'US'])."
659
+ },
660
+ device_type: {
661
+ type: "array",
662
+ items: { type: "string", minLength: 1 },
663
+ minItems: 1,
664
+ description: "Filter by device type(s) (e.g. ['mobile', 'desktop'])."
665
+ },
666
+ browser: {
667
+ type: "array",
668
+ items: { type: "string", minLength: 1 },
669
+ minItems: 1,
670
+ description: "Filter by browser name(s)."
671
+ },
672
+ os: {
673
+ type: "array",
674
+ items: { type: "string", minLength: 1 },
675
+ minItems: 1,
676
+ description: "Filter by operating system name(s)."
677
+ },
678
+ channel_group: {
679
+ type: "array",
680
+ items: { type: "string", minLength: 1 },
681
+ minItems: 1,
682
+ description: "Filter by channel group(s)."
683
+ },
684
+ utm_source: {
685
+ type: "array",
686
+ items: { type: "string", minLength: 1 },
687
+ minItems: 1,
688
+ description: "Filter by UTM source(s)."
689
+ },
690
+ utm_medium: {
691
+ type: "array",
692
+ items: { type: "string", minLength: 1 },
693
+ minItems: 1,
694
+ description: "Filter by UTM medium(s)."
695
+ },
696
+ utm_campaign: {
697
+ type: "array",
698
+ items: { type: "string", minLength: 1 },
699
+ minItems: 1,
700
+ description: "Filter by UTM campaign(s)."
701
+ },
702
+ utm_term: {
703
+ type: "array",
704
+ items: { type: "string", minLength: 1 },
705
+ minItems: 1,
706
+ description: "Filter by UTM term(s)."
707
+ },
708
+ // `minItems` intentionally absent — empty array ≡ no grouping (PRD 08
709
+ // VAL-006 exception). Filter arrays use minItems: 1 because an empty
710
+ // filter list would silently match nothing; `include` instead is a no-op
711
+ // when empty, matching the API behavior.
712
+ include: {
713
+ type: "array",
714
+ items: { type: "string", enum: ["device", "browser", "os", "channel_group"] },
715
+ description: "Add dimension(s) to GROUP BY and response. Allowed values: device, browser, os, channel_group. Empty/absent means no extra grouping."
716
+ }
717
+ };
718
+ var MULTI_FILTER_KEYS = [
719
+ "country",
720
+ "device_type",
721
+ "browser",
722
+ "os",
723
+ "channel_group",
724
+ "utm_source",
725
+ "utm_medium",
726
+ "utm_campaign",
727
+ "utm_term",
728
+ "include"
729
+ ];
730
+ function pickMultiFilters(args) {
731
+ const out = {};
732
+ for (const key of MULTI_FILTER_KEYS) {
733
+ const value = args[key];
734
+ if (Array.isArray(value) && value.length > 0) {
735
+ out[key] = value;
736
+ }
737
+ }
738
+ return out;
739
+ }
740
+ var getPagesTool = {
741
+ name: "get_pages",
742
+ description: "Get metrics per page URL path: pageviews and entrances. Useful for finding most popular pages and content performance. For bounce rate by entry page, use get_landing_pages instead \u2014 bounce is not interpretable at page granularity. Supports multi-value filters (country, device_type, browser, os, channel_group, UTMs) as arrays of strings \u2014 e.g. device_type=['mobile'] to filter to mobile only. Pass include=['device'] to also break down metrics by device. The two are orthogonal \u2014 combine them as needed.",
743
+ inputSchema: {
744
+ type: "object",
745
+ properties: {
746
+ site_id: {
747
+ type: "string",
748
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
749
+ },
750
+ period: PERIOD_SCHEMA,
751
+ compare: COMPARE_SCHEMA,
752
+ limit: LIMIT_SCHEMA,
753
+ sort_by: {
754
+ type: "string",
755
+ description: "Field to sort by.",
756
+ enum: ["page_views", "entrances"],
757
+ default: "page_views"
758
+ },
759
+ sort_order: SORT_ORDER_SCHEMA,
760
+ page: PAGE_SCHEMA,
761
+ path_filter: {
762
+ type: "string",
763
+ description: "Filter pages by URL path pattern (e.g. '/blog')."
764
+ },
765
+ ...MULTI_FILTER_SCHEMA
766
+ }
767
+ },
768
+ handler: async (client, args) => {
769
+ return client.requestPaginated("/stats/pages", {
770
+ site_id: resolveSiteId(args),
771
+ period: args.period ?? "30d",
772
+ compare: args.compare,
773
+ page_size: String(args.limit ?? 20),
774
+ page: args.page != null ? String(args.page) : void 0,
775
+ sort_by: args.sort_by ?? "page_views",
776
+ sort_order: args.sort_order ?? "desc",
777
+ path_filter: args.path_filter,
778
+ ...pickMultiFilters(args)
58
779
  });
59
- const response = await fetch(url.toString(), {
60
- headers: {
61
- Authorization: `Bearer ${token}`,
62
- Accept: "application/json",
63
- },
780
+ }
781
+ };
782
+ var getLandingPagesTool = {
783
+ name: "get_landing_pages",
784
+ description: "Get landing page performance: entrances, bounce rate, conversions. Landing pages are the first page users see when entering the site. High bounce rates indicate poor landing page experience. Supports multi-value filters (country, device_type, browser, os, channel_group, UTMs) as arrays of strings \u2014 e.g. country=['ES','PT'] to filter to two countries. Pass include=['channel_group'] to also break down by channel group. The two are orthogonal \u2014 combine them as needed.",
785
+ inputSchema: {
786
+ type: "object",
787
+ properties: {
788
+ site_id: {
789
+ type: "string",
790
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
791
+ },
792
+ period: PERIOD_SCHEMA,
793
+ compare: COMPARE_SCHEMA,
794
+ limit: LIMIT_SCHEMA,
795
+ sort_by: {
796
+ type: "string",
797
+ description: "Field to sort by.",
798
+ enum: ["entrances", "engaged_entrances", "page_views", "conversions", "revenue", "bounce_rate"],
799
+ default: "entrances"
800
+ },
801
+ sort_order: SORT_ORDER_SCHEMA,
802
+ page: PAGE_SCHEMA,
803
+ path_filter: {
804
+ type: "string",
805
+ description: "Filter by URL path pattern."
806
+ },
807
+ ...MULTI_FILTER_SCHEMA
808
+ }
809
+ },
810
+ handler: async (client, args) => {
811
+ return client.requestPaginated("/stats/landing-pages", {
812
+ site_id: resolveSiteId(args),
813
+ period: args.period ?? "30d",
814
+ compare: args.compare,
815
+ page_size: String(args.limit ?? 20),
816
+ page: args.page != null ? String(args.page) : void 0,
817
+ sort_by: args.sort_by ?? "entrances",
818
+ sort_order: args.sort_order ?? "desc",
819
+ path_filter: args.path_filter,
820
+ ...pickMultiFilters(args)
64
821
  });
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));
822
+ }
823
+ };
824
+ var getTopPagesTool = {
825
+ name: "get_top_pages",
826
+ description: "Get top pages ranked by page views. Returns a compact list of the top N pages \u2014 ideal for quick rankings and summaries.",
827
+ inputSchema: {
828
+ type: "object",
829
+ properties: {
830
+ site_id: {
831
+ type: "string",
832
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
833
+ },
834
+ period: PERIOD_SCHEMA,
835
+ limit: LIMIT_SCHEMA,
836
+ country: {
837
+ type: "string",
838
+ description: "Filter by country code (e.g. 'ES', 'US')."
839
+ },
840
+ utm_source: {
841
+ type: "string",
842
+ description: "Filter by UTM source."
843
+ },
844
+ utm_medium: {
845
+ type: "string",
846
+ description: "Filter by UTM medium."
847
+ },
848
+ utm_campaign: {
849
+ type: "string",
850
+ description: "Filter by UTM campaign."
851
+ },
852
+ utm_term: {
853
+ type: "string",
854
+ description: "Filter by UTM term."
855
+ }
94
856
  }
95
- return false;
857
+ },
858
+ handler: async (client, args) => {
859
+ return client.request("/stats/pages/top", {
860
+ site_id: resolveSiteId(args),
861
+ period: args.period ?? "30d",
862
+ limit: String(args.limit ?? 10),
863
+ country: args.country,
864
+ utm_source: args.utm_source,
865
+ utm_medium: args.utm_medium,
866
+ utm_campaign: args.utm_campaign,
867
+ utm_term: args.utm_term
868
+ });
869
+ }
870
+ };
871
+ var getTopLandingPagesTool = {
872
+ name: "get_top_landing_pages",
873
+ description: "Get top landing pages ranked by entrances. Returns a compact list of the top N landing pages \u2014 ideal for quick rankings.",
874
+ inputSchema: {
875
+ type: "object",
876
+ properties: {
877
+ site_id: {
878
+ type: "string",
879
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
880
+ },
881
+ period: PERIOD_SCHEMA,
882
+ limit: LIMIT_SCHEMA,
883
+ utm_source: {
884
+ type: "string",
885
+ description: "Filter by UTM source."
886
+ },
887
+ utm_medium: {
888
+ type: "string",
889
+ description: "Filter by UTM medium."
890
+ },
891
+ country: {
892
+ type: "string",
893
+ description: "Filter by country code (e.g. 'ES', 'US')."
894
+ },
895
+ content_grouping: {
896
+ type: "string",
897
+ description: "Filter by content grouping name."
898
+ }
899
+ }
900
+ },
901
+ handler: async (client, args) => {
902
+ return client.request("/stats/landing-pages/top", {
903
+ site_id: resolveSiteId(args),
904
+ period: args.period ?? "30d",
905
+ limit: String(args.limit ?? 10),
906
+ utm_source: args.utm_source,
907
+ utm_medium: args.utm_medium,
908
+ country: args.country,
909
+ content_grouping: args.content_grouping
910
+ });
911
+ }
912
+ };
913
+ var getLandingPagesByContentGroupTool = {
914
+ name: "get_landing_pages_by_content_group",
915
+ description: "Get landing page metrics grouped by content grouping. Shows aggregate performance per content group (e.g. blog, product, category).",
916
+ inputSchema: {
917
+ type: "object",
918
+ properties: {
919
+ site_id: {
920
+ type: "string",
921
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
922
+ },
923
+ period: PERIOD_SCHEMA,
924
+ utm_source: {
925
+ type: "string",
926
+ description: "Filter by UTM source."
927
+ },
928
+ utm_medium: {
929
+ type: "string",
930
+ description: "Filter by UTM medium."
931
+ },
932
+ country: {
933
+ type: "string",
934
+ description: "Filter by country code (e.g. 'ES', 'US')."
935
+ }
936
+ }
937
+ },
938
+ handler: async (client, args) => {
939
+ return client.request("/stats/landing-pages/by-content-group", {
940
+ site_id: resolveSiteId(args),
941
+ period: args.period ?? "30d",
942
+ utm_source: args.utm_source,
943
+ utm_medium: args.utm_medium,
944
+ country: args.country
945
+ });
946
+ }
947
+ };
948
+
949
+ // dist/tools/conversions.js
950
+ var getConversionsTool = {
951
+ name: "get_conversions",
952
+ description: "Get conversions broken down by type (e.g. purchase, signup). Shows count, revenue, and average order value per conversion type. Use for aggregated counts/revenue by conversion type. **For per-event detail or per-product analysis use `get_conversions_raw` or `get_conversion_items_raw`.**",
953
+ inputSchema: {
954
+ type: "object",
955
+ properties: {
956
+ site_id: {
957
+ type: "string",
958
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
959
+ },
960
+ period: PERIOD_SCHEMA,
961
+ compare: COMPARE_SCHEMA,
962
+ limit: LIMIT_SCHEMA,
963
+ sort_by: {
964
+ type: "string",
965
+ description: "Field to sort by.",
966
+ enum: ["count", "revenue", "avg_value"],
967
+ default: "count"
968
+ },
969
+ sort_order: SORT_ORDER_SCHEMA,
970
+ page: PAGE_SCHEMA,
971
+ utm_source: {
972
+ type: "string",
973
+ description: "Filter by traffic source."
974
+ },
975
+ utm_medium: {
976
+ type: "string",
977
+ description: "Filter by traffic medium."
978
+ },
979
+ country: {
980
+ type: "string",
981
+ description: "Filter by country code (e.g. 'ES', 'US')."
982
+ }
983
+ }
984
+ },
985
+ handler: async (client, args) => {
986
+ return client.requestPaginated("/stats/conversions", {
987
+ site_id: resolveSiteId(args),
988
+ period: args.period ?? "30d",
989
+ compare: args.compare,
990
+ page_size: String(args.limit ?? 20),
991
+ page: args.page != null ? String(args.page) : void 0,
992
+ sort_by: args.sort_by ?? "count",
993
+ sort_order: args.sort_order ?? "desc",
994
+ utm_source: args.utm_source,
995
+ utm_medium: args.utm_medium,
996
+ country: args.country
997
+ });
998
+ }
999
+ };
1000
+ var getMicroconversionsTool = {
1001
+ name: "get_microconversions",
1002
+ description: "Get microconversions (smaller engagement events) broken down by type: add_to_cart, newsletter_signup, pdf_download, etc. Shows count and trends per event type. **For per-event detail use `get_microconversions_raw`.**",
1003
+ inputSchema: {
1004
+ type: "object",
1005
+ properties: {
1006
+ site_id: {
1007
+ type: "string",
1008
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1009
+ },
1010
+ period: PERIOD_SCHEMA,
1011
+ compare: COMPARE_SCHEMA,
1012
+ limit: LIMIT_SCHEMA,
1013
+ sort_by: {
1014
+ type: "string",
1015
+ description: "Field to sort by.",
1016
+ enum: ["count", "revenue", "avg_value"],
1017
+ default: "count"
1018
+ },
1019
+ sort_order: SORT_ORDER_SCHEMA,
1020
+ page: PAGE_SCHEMA,
1021
+ conversion_type: {
1022
+ type: "string",
1023
+ description: "Filter by specific microconversion type."
1024
+ }
1025
+ }
1026
+ },
1027
+ handler: async (client, args) => {
1028
+ return client.requestPaginated("/stats/microconversions", {
1029
+ site_id: resolveSiteId(args),
1030
+ period: args.period ?? "30d",
1031
+ compare: args.compare,
1032
+ page_size: String(args.limit ?? 20),
1033
+ page: args.page != null ? String(args.page) : void 0,
1034
+ sort_by: args.sort_by ?? "count",
1035
+ sort_order: args.sort_order ?? "desc",
1036
+ conversion_type: args.conversion_type
1037
+ });
1038
+ }
1039
+ };
1040
+ var listMicroconversionTypesTool = {
1041
+ name: "list_microconversion_types",
1042
+ description: "Get the list of available microconversion type names for a site. Use this to discover what microconversion types exist before querying details.",
1043
+ inputSchema: {
1044
+ type: "object",
1045
+ properties: {
1046
+ site_id: {
1047
+ type: "string",
1048
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1049
+ },
1050
+ period: PERIOD_SCHEMA
1051
+ }
1052
+ },
1053
+ handler: async (client, args) => {
1054
+ return client.request("/stats/microconversions-types", {
1055
+ site_id: resolveSiteId(args),
1056
+ period: args.period ?? "30d"
1057
+ });
1058
+ }
1059
+ };
1060
+ var getMicroconversionDetailsTool = {
1061
+ name: "get_microconversion_details",
1062
+ description: "Get detailed breakdown for a specific microconversion type. Shows metrics segmented by source, medium, campaign, country, device, browser, and OS.",
1063
+ inputSchema: {
1064
+ type: "object",
1065
+ properties: {
1066
+ site_id: {
1067
+ type: "string",
1068
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1069
+ },
1070
+ conversion_type: {
1071
+ type: "string",
1072
+ description: "The microconversion type to get details for (e.g. 'add_to_cart', 'newsletter_signup')."
1073
+ },
1074
+ period: PERIOD_SCHEMA,
1075
+ utm_source: {
1076
+ type: "string",
1077
+ description: "Filter by UTM source."
1078
+ },
1079
+ utm_medium: {
1080
+ type: "string",
1081
+ description: "Filter by UTM medium."
1082
+ },
1083
+ utm_campaign: {
1084
+ type: "string",
1085
+ description: "Filter by UTM campaign."
1086
+ },
1087
+ utm_term: {
1088
+ type: "string",
1089
+ description: "Filter by UTM term."
1090
+ },
1091
+ country: {
1092
+ type: "string",
1093
+ description: "Filter by country code (e.g. 'ES', 'US')."
1094
+ },
1095
+ device_type: {
1096
+ type: "string",
1097
+ description: "Filter by device type (e.g. 'desktop', 'mobile', 'tablet')."
1098
+ },
1099
+ browser: {
1100
+ type: "string",
1101
+ description: "Filter by browser name."
1102
+ },
1103
+ os: {
1104
+ type: "string",
1105
+ description: "Filter by operating system name."
1106
+ }
1107
+ },
1108
+ required: ["conversion_type"]
1109
+ },
1110
+ handler: async (client, args) => {
1111
+ const conversionType = args.conversion_type;
1112
+ return client.request(`/stats/microconversions/${encodeURIComponent(conversionType)}`, {
1113
+ site_id: resolveSiteId(args),
1114
+ period: args.period ?? "30d",
1115
+ utm_source: args.utm_source,
1116
+ utm_medium: args.utm_medium,
1117
+ utm_campaign: args.utm_campaign,
1118
+ utm_term: args.utm_term,
1119
+ country: args.country,
1120
+ device_type: args.device_type,
1121
+ browser: args.browser,
1122
+ os: args.os
1123
+ });
1124
+ }
1125
+ };
1126
+ var RAW_LIMIT_SCHEMA = {
1127
+ type: "number",
1128
+ description: "Maximum number of rows to return (default: 10, max: 100). MCP-side cap to control token usage.",
1129
+ default: 10,
1130
+ minimum: 1,
1131
+ maximum: 100
1132
+ };
1133
+ var RAW_FILTER_KEYS = [
1134
+ "conversion_type",
1135
+ "country",
1136
+ "device_type",
1137
+ "browser",
1138
+ "os",
1139
+ "channel_group",
1140
+ "utm_source",
1141
+ "utm_medium",
1142
+ "utm_campaign",
1143
+ "utm_term",
1144
+ "utm_content"
1145
+ ];
1146
+ function pickRawFilters(args) {
1147
+ const out = {};
1148
+ for (const key of RAW_FILTER_KEYS) {
1149
+ const value = args[key];
1150
+ if (Array.isArray(value) && value.length > 0) {
1151
+ out[key] = value;
1152
+ }
1153
+ }
1154
+ return out;
96
1155
  }
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
- },
1156
+ function buildRawDates(args) {
1157
+ const startDate = args.start_date;
1158
+ const endDate = args.end_date;
1159
+ if (startDate && endDate) {
1160
+ return { start_date: startDate, end_date: endDate };
1161
+ }
1162
+ return { period: args.period ?? "30d" };
1163
+ }
1164
+ var RAW_DATE_SCHEMA = {
1165
+ period: PERIOD_SCHEMA,
1166
+ start_date: {
1167
+ type: "string",
1168
+ description: "Custom start date (YYYY-MM-DD). Use together with end_date as an alternative to period. Date range capped at 31 days by the API."
1169
+ },
1170
+ end_date: {
1171
+ type: "string",
1172
+ description: "Custom end date (YYYY-MM-DD). Use together with start_date."
1173
+ }
1174
+ };
1175
+ var RAW_MULTI_FILTER_SCHEMA = {
1176
+ conversion_type: {
1177
+ type: "array",
1178
+ items: { type: "string", minLength: 1 },
1179
+ minItems: 1,
1180
+ description: "Filter by conversion type (e.g. ['purchase'])."
1181
+ },
1182
+ country: {
1183
+ type: "array",
1184
+ items: { type: "string", minLength: 1 },
1185
+ minItems: 1,
1186
+ description: "Filter by country code (e.g. ['ES', 'US'])."
1187
+ },
1188
+ device_type: {
1189
+ type: "array",
1190
+ items: { type: "string", minLength: 1 },
1191
+ minItems: 1,
1192
+ description: "Filter by device type (e.g. ['mobile'])."
1193
+ },
1194
+ browser: {
1195
+ type: "array",
1196
+ items: { type: "string", minLength: 1 },
1197
+ minItems: 1,
1198
+ description: "Filter by browser."
1199
+ },
1200
+ os: {
1201
+ type: "array",
1202
+ items: { type: "string", minLength: 1 },
1203
+ minItems: 1,
1204
+ description: "Filter by operating system."
1205
+ },
1206
+ channel_group: {
1207
+ type: "array",
1208
+ items: { type: "string", minLength: 1 },
1209
+ minItems: 1,
1210
+ description: "Filter by channel group."
1211
+ },
1212
+ utm_source: {
1213
+ type: "array",
1214
+ items: { type: "string", minLength: 1 },
1215
+ minItems: 1,
1216
+ description: "Filter by UTM source."
1217
+ },
1218
+ utm_medium: {
1219
+ type: "array",
1220
+ items: { type: "string", minLength: 1 },
1221
+ minItems: 1,
1222
+ description: "Filter by UTM medium."
1223
+ },
1224
+ utm_campaign: {
1225
+ type: "array",
1226
+ items: { type: "string", minLength: 1 },
1227
+ minItems: 1,
1228
+ description: "Filter by UTM campaign."
1229
+ },
1230
+ utm_term: {
1231
+ type: "array",
1232
+ items: { type: "string", minLength: 1 },
1233
+ minItems: 1,
1234
+ description: "Filter by UTM term."
1235
+ },
1236
+ utm_content: {
1237
+ type: "array",
1238
+ items: { type: "string", minLength: 1 },
1239
+ minItems: 1,
1240
+ description: "Filter by UTM content."
1241
+ }
1242
+ };
1243
+ function stripProperties(result) {
1244
+ return {
1245
+ ...result,
1246
+ data: result.data.map((row) => {
1247
+ const { properties: _omit, ...rest } = row;
1248
+ return rest;
1249
+ })
1250
+ };
1251
+ }
1252
+ var getConversionsRawTool = {
1253
+ name: "get_conversions_raw",
1254
+ description: "Returns raw conversion rows from /stats/conversions/raw (one row per event, with timestamp_utc and timestamp_local). Use for one-row-per-event detail. **For per-product/SKU analysis prefer `get_conversion_items_raw` (always includes item properties like sku, price, quantity).** Custom `properties` are excluded by default \u2014 pass `include_properties=true` to receive them. Date range capped at 31 days; page_size capped at 100 to control token usage.",
1255
+ inputSchema: {
1256
+ type: "object",
1257
+ properties: {
1258
+ site_id: {
1259
+ type: "string",
1260
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1261
+ },
1262
+ ...RAW_DATE_SCHEMA,
1263
+ page: PAGE_SCHEMA,
1264
+ limit: RAW_LIMIT_SCHEMA,
1265
+ include_properties: {
1266
+ type: "boolean",
1267
+ description: "If true, include the custom `properties` object in each row. Default false to keep responses compact.",
1268
+ default: false
1269
+ },
1270
+ ...RAW_MULTI_FILTER_SCHEMA
1271
+ }
1272
+ },
1273
+ handler: async (client, args) => {
1274
+ const limit = args.limit ?? 10;
1275
+ const includeProperties = args.include_properties === true;
1276
+ const dates = buildRawDates(args);
1277
+ const result = await client.requestPaginated("/stats/conversions/raw", {
1278
+ site_id: resolveSiteId(args),
1279
+ ...dates,
1280
+ page_size: String(limit),
1281
+ page: args.page != null ? String(args.page) : void 0,
1282
+ ...pickRawFilters(args)
1283
+ });
1284
+ return includeProperties ? result : stripProperties(result);
1285
+ }
1286
+ };
1287
+ var getMicroconversionsRawTool = {
1288
+ name: "get_microconversions_raw",
1289
+ description: "Returns raw microconversion rows from /stats/microconversions/raw (one row per event, with timestamp_utc and timestamp_local). Use for one-row-per-event detail of microconversions (add_to_cart, newsletter_signup, etc.). Custom `properties` are excluded by default \u2014 pass `include_properties=true` to receive them. Date range capped at 31 days; page_size capped at 100 to control token usage.",
1290
+ inputSchema: {
1291
+ type: "object",
1292
+ properties: {
1293
+ site_id: {
1294
+ type: "string",
1295
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1296
+ },
1297
+ ...RAW_DATE_SCHEMA,
1298
+ page: PAGE_SCHEMA,
1299
+ limit: RAW_LIMIT_SCHEMA,
1300
+ include_properties: {
1301
+ type: "boolean",
1302
+ description: "If true, include the custom `properties` object in each row. Default false to keep responses compact.",
1303
+ default: false
1304
+ },
1305
+ ...RAW_MULTI_FILTER_SCHEMA
1306
+ }
1307
+ },
1308
+ handler: async (client, args) => {
1309
+ const limit = args.limit ?? 10;
1310
+ const includeProperties = args.include_properties === true;
1311
+ const dates = buildRawDates(args);
1312
+ const result = await client.requestPaginated("/stats/microconversions/raw", {
1313
+ site_id: resolveSiteId(args),
1314
+ ...dates,
1315
+ page_size: String(limit),
1316
+ page: args.page != null ? String(args.page) : void 0,
1317
+ ...pickRawFilters(args)
1318
+ });
1319
+ return includeProperties ? result : stripProperties(result);
1320
+ }
1321
+ };
1322
+ var getConversionItemsRawTool = {
1323
+ name: "get_conversion_items_raw",
1324
+ description: "Returns one row per item inside a conversion (e.g. one row per product in a purchase) from /stats/conversion-items/raw. `properties` always included \u2014 that's where product_id, sku, price, quantity live. **Best tool for per-product analytics.** Date range capped at 31 days; page_size capped at 100 to control token usage.",
1325
+ inputSchema: {
1326
+ type: "object",
1327
+ properties: {
1328
+ site_id: {
1329
+ type: "string",
1330
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1331
+ },
1332
+ ...RAW_DATE_SCHEMA,
1333
+ page: PAGE_SCHEMA,
1334
+ limit: RAW_LIMIT_SCHEMA,
1335
+ ...RAW_MULTI_FILTER_SCHEMA
1336
+ }
1337
+ },
1338
+ handler: async (client, args) => {
1339
+ const limit = args.limit ?? 10;
1340
+ const dates = buildRawDates(args);
1341
+ return client.requestPaginated("/stats/conversion-items/raw", {
1342
+ site_id: resolveSiteId(args),
1343
+ ...dates,
1344
+ page_size: String(limit),
1345
+ page: args.page != null ? String(args.page) : void 0,
1346
+ ...pickRawFilters(args)
1347
+ });
1348
+ }
1349
+ };
1350
+
1351
+ // dist/tools/audience.js
1352
+ var getCountriesTool = {
1353
+ name: "get_countries",
1354
+ description: "Get traffic broken down by country. Shows entrances, pageviews, conversions, and revenue per country. Country codes follow ISO 3166-1 alpha-2 (e.g. ES=Spain, US=United States).",
1355
+ inputSchema: {
1356
+ type: "object",
1357
+ properties: {
1358
+ site_id: {
1359
+ type: "string",
1360
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1361
+ },
1362
+ period: PERIOD_SCHEMA,
1363
+ compare: COMPARE_SCHEMA,
1364
+ limit: LIMIT_SCHEMA,
1365
+ sort_by: {
1366
+ type: "string",
1367
+ description: "Field to sort by.",
1368
+ enum: ["entrances", "engaged_entrances", "page_views", "conversions", "revenue", "bounce_rate"],
1369
+ default: "entrances"
1370
+ },
1371
+ sort_order: SORT_ORDER_SCHEMA,
1372
+ page: PAGE_SCHEMA
1373
+ }
1374
+ },
1375
+ handler: async (client, args) => {
1376
+ return client.requestPaginated("/stats/geo/countries", {
1377
+ site_id: resolveSiteId(args),
1378
+ period: args.period ?? "30d",
1379
+ compare: args.compare,
1380
+ page_size: String(args.limit ?? 20),
1381
+ page: args.page != null ? String(args.page) : void 0,
1382
+ sort_by: args.sort_by ?? "entrances",
1383
+ sort_order: args.sort_order ?? "desc"
1384
+ });
1385
+ }
1386
+ };
1387
+ var getDevicesTool = {
1388
+ name: "get_devices",
1389
+ description: "Get traffic broken down by device type (desktop/mobile/tablet), browser (Chrome, Safari, Firefox...), and operating system (Windows, macOS, iOS, Android...). Returns all three breakdowns in a single call.",
1390
+ inputSchema: {
1391
+ type: "object",
1392
+ properties: {
1393
+ site_id: {
1394
+ type: "string",
1395
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1396
+ },
1397
+ period: PERIOD_SCHEMA,
1398
+ compare: COMPARE_SCHEMA
1399
+ }
1400
+ },
1401
+ handler: async (client, args) => {
1402
+ return client.request("/stats/devices", {
1403
+ site_id: resolveSiteId(args),
1404
+ period: args.period ?? "30d",
1405
+ compare: args.compare
1406
+ });
1407
+ }
1408
+ };
1409
+ var getBrowsersTool = {
1410
+ name: "get_browsers",
1411
+ description: "Get traffic broken down by browser: Chrome, Safari, Firefox, Edge, etc. Shows entrances and pageviews per browser with pagination.",
1412
+ inputSchema: {
1413
+ type: "object",
1414
+ properties: {
1415
+ site_id: {
1416
+ type: "string",
1417
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1418
+ },
1419
+ period: PERIOD_SCHEMA,
1420
+ limit: LIMIT_SCHEMA,
1421
+ page: PAGE_SCHEMA,
1422
+ country: {
1423
+ type: "string",
1424
+ description: "Filter by country code (e.g. 'ES', 'US')."
1425
+ }
1426
+ }
1427
+ },
1428
+ handler: async (client, args) => {
1429
+ return client.requestPaginated("/stats/browsers", {
1430
+ site_id: resolveSiteId(args),
1431
+ period: args.period ?? "30d",
1432
+ page_size: String(args.limit ?? 20),
1433
+ page: args.page != null ? String(args.page) : void 0,
1434
+ country: args.country
1435
+ });
1436
+ }
1437
+ };
1438
+ var getOperatingSystemsTool = {
1439
+ name: "get_operating_systems",
1440
+ description: "Get traffic broken down by operating system: Windows, macOS, iOS, Android, Linux, etc. Shows entrances and pageviews per OS with pagination.",
1441
+ inputSchema: {
1442
+ type: "object",
1443
+ properties: {
1444
+ site_id: {
1445
+ type: "string",
1446
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1447
+ },
1448
+ period: PERIOD_SCHEMA,
1449
+ limit: LIMIT_SCHEMA,
1450
+ page: PAGE_SCHEMA,
1451
+ country: {
1452
+ type: "string",
1453
+ description: "Filter by country code (e.g. 'ES', 'US')."
1454
+ }
1455
+ }
1456
+ },
1457
+ handler: async (client, args) => {
1458
+ return client.requestPaginated("/stats/operating-systems", {
1459
+ site_id: resolveSiteId(args),
1460
+ period: args.period ?? "30d",
1461
+ page_size: String(args.limit ?? 20),
1462
+ page: args.page != null ? String(args.page) : void 0,
1463
+ country: args.country
1464
+ });
1465
+ }
1466
+ };
1467
+ var getDeviceTypesTool = {
1468
+ name: "get_device_types",
1469
+ description: "Get traffic broken down by device type (desktop, mobile, tablet) with pagination. Shows entrances, pageviews, conversions per device type.",
1470
+ inputSchema: {
1471
+ type: "object",
1472
+ properties: {
1473
+ site_id: {
1474
+ type: "string",
1475
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1476
+ },
1477
+ period: PERIOD_SCHEMA,
1478
+ limit: LIMIT_SCHEMA,
1479
+ page: PAGE_SCHEMA,
1480
+ country: {
1481
+ type: "string",
1482
+ description: "Filter by country code (e.g. 'ES', 'US')."
1483
+ }
1484
+ }
1485
+ },
1486
+ handler: async (client, args) => {
1487
+ return client.requestPaginated("/stats/devices/types", {
1488
+ site_id: resolveSiteId(args),
1489
+ period: args.period ?? "30d",
1490
+ page_size: String(args.limit ?? 10),
1491
+ page: args.page != null ? String(args.page) : void 0,
1492
+ country: args.country
1493
+ });
1494
+ }
1495
+ };
1496
+
1497
+ // dist/tools/funnel.js
1498
+ var getFunnelTool = {
1499
+ name: "get_funnel",
1500
+ description: "Get funnel analysis showing conversion rates between steps. Returns step-by-step data including visitor count, conversion rate, and dropoff at each stage. Useful for finding where users drop off in multi-step flows.",
1501
+ inputSchema: {
1502
+ type: "object",
1503
+ properties: {
1504
+ site_id: {
1505
+ type: "string",
1506
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1507
+ },
1508
+ period: PERIOD_SCHEMA,
1509
+ country: {
1510
+ type: "string",
1511
+ description: "Filter by country code (e.g. 'ES')."
1512
+ }
1513
+ }
1514
+ },
1515
+ handler: async (client, args) => {
1516
+ return client.request("/stats/funnel", {
1517
+ site_id: resolveSiteId(args),
1518
+ period: args.period ?? "30d",
1519
+ country: args.country
1520
+ });
1521
+ }
1522
+ };
1523
+
1524
+ // dist/tools/content.js
1525
+ var getContentGroupsTool = {
1526
+ name: "get_content_groups",
1527
+ description: "Get metrics grouped by content group (content_grouping). Shows pageviews and entrances per content group. Useful for understanding which sections of a site get the most traffic.",
1528
+ inputSchema: {
1529
+ type: "object",
1530
+ properties: {
1531
+ site_id: {
1532
+ type: "string",
1533
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1534
+ },
1535
+ period: PERIOD_SCHEMA,
1536
+ country: {
1537
+ type: "string",
1538
+ description: "Filter by country code (e.g. 'ES', 'US')."
1539
+ },
1540
+ utm_source: {
1541
+ type: "string",
1542
+ description: "Filter by UTM source (e.g. 'google')."
1543
+ },
1544
+ utm_medium: {
1545
+ type: "string",
1546
+ description: "Filter by UTM medium (e.g. 'cpc')."
1547
+ },
1548
+ utm_campaign: {
1549
+ type: "string",
1550
+ description: "Filter by UTM campaign name."
1551
+ },
1552
+ utm_term: {
1553
+ type: "string",
1554
+ description: "Filter by UTM term."
1555
+ }
1556
+ }
1557
+ },
1558
+ handler: async (client, args) => {
1559
+ return client.request("/stats/pages/content-groups", {
1560
+ site_id: resolveSiteId(args),
1561
+ period: args.period ?? "30d",
1562
+ country: args.country,
1563
+ utm_source: args.utm_source,
1564
+ utm_medium: args.utm_medium,
1565
+ utm_campaign: args.utm_campaign,
1566
+ utm_term: args.utm_term
1567
+ });
1568
+ }
1569
+ };
1570
+
1571
+ // dist/tools/channels.js
1572
+ var getChannelsTool = {
1573
+ name: "get_channels",
1574
+ description: "Get traffic metrics grouped by channel: Paid Search, Organic Search, Social, Direct, Email, Referral, etc. Channels are automatically classified based on UTM parameters and referrer rules.",
1575
+ inputSchema: {
1576
+ type: "object",
1577
+ properties: {
1578
+ site_id: {
1579
+ type: "string",
1580
+ description: "Site ID (account_id). Optional if SEALMETRICS_SITE_ID env var is set."
1581
+ },
1582
+ period: PERIOD_SCHEMA,
1583
+ limit: LIMIT_SCHEMA,
1584
+ page: PAGE_SCHEMA,
1585
+ country: {
1586
+ type: "string",
1587
+ description: "Filter by country code (e.g. 'ES', 'US')."
1588
+ }
1589
+ }
1590
+ },
1591
+ handler: async (client, args) => {
1592
+ return client.request("/channel-groups/stats/channels", {
1593
+ account_id: resolveSiteId(args),
1594
+ period: args.period ?? "30d",
1595
+ page_size: String(args.limit ?? 20),
1596
+ page: args.page != null ? String(args.page) : void 0,
1597
+ country: args.country
1598
+ });
1599
+ }
1600
+ };
1601
+ var getTopChannelsTool = {
1602
+ name: "get_top_channels",
1603
+ description: "Get top channels ranked by entrances. Returns a compact list of the top N channels (Paid Search, Organic, Social, etc.) \u2014 ideal for quick rankings.",
1604
+ inputSchema: {
1605
+ type: "object",
1606
+ properties: {
1607
+ site_id: {
1608
+ type: "string",
1609
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
1610
+ },
1611
+ period: PERIOD_SCHEMA,
1612
+ limit: LIMIT_SCHEMA,
1613
+ country: {
1614
+ type: "string",
1615
+ description: "Filter by country code (e.g. 'ES', 'US')."
1616
+ }
1617
+ }
1618
+ },
1619
+ handler: async (client, args) => {
1620
+ return client.request("/stats/top-channels", {
1621
+ site_id: resolveSiteId(args),
1622
+ period: args.period ?? "30d",
1623
+ limit: String(args.limit ?? 10),
1624
+ country: args.country
1625
+ });
1626
+ }
1627
+ };
1628
+ var listChannelRulesTool = {
1629
+ name: "list_channel_rules",
1630
+ description: "List channel group rules configured for a site. Shows how traffic is classified into channels (Paid Search, Organic, Social, etc.) based on UTM parameters and referrer patterns.",
1631
+ inputSchema: {
1632
+ type: "object",
1633
+ properties: {
1634
+ site_id: {
1635
+ type: "string",
1636
+ description: "Site ID (account_id). Optional if SEALMETRICS_SITE_ID env var is set."
1637
+ },
1638
+ include_inactive: {
1639
+ type: "boolean",
1640
+ description: "Include inactive rules (default: false)."
1641
+ },
1642
+ include_defaults: {
1643
+ type: "boolean",
1644
+ description: "Include default system rules (default: true)."
1645
+ }
1646
+ }
1647
+ },
1648
+ handler: async (client, args) => {
1649
+ return client.request("/channel-groups", {
1650
+ account_id: resolveSiteId(args),
1651
+ include_inactive: args.include_inactive != null ? String(args.include_inactive) : void 0,
1652
+ include_defaults: args.include_defaults != null ? String(args.include_defaults) : void 0
1653
+ });
1654
+ }
1655
+ };
1656
+
1657
+ // dist/tools/bots.js
1658
+ var getBotStatsTool = {
1659
+ name: "get_bot_stats",
1660
+ description: "Get bot detection overview: score distribution, top suspicion flags, and daily trend of human vs suspected-bot traffic. Requires agent analytics to be enabled on the site.",
1661
+ inputSchema: {
1662
+ type: "object",
1663
+ properties: {
1664
+ site_id: {
1665
+ type: "string",
1666
+ description: "Site ID (account_id). Optional if SEALMETRICS_SITE_ID env var is set."
1667
+ },
1668
+ days: {
1669
+ type: "number",
1670
+ description: "Number of days to analyze (1-90, default: 30).",
1671
+ default: 30
1672
+ }
1673
+ }
1674
+ },
1675
+ handler: async (client, args) => {
1676
+ return client.request("/bot-stats/overview", {
1677
+ account_id: resolveSiteId(args),
1678
+ days: String(args.days ?? 30)
1679
+ });
1680
+ }
1681
+ };
1682
+ var getSuspiciousSessionsTool = {
1683
+ name: "get_suspicious_sessions",
1684
+ description: "Get sessions with high bot-suspicion scores for investigation. Shows session details, score, and detected flags. Useful for identifying automated traffic patterns.",
1685
+ inputSchema: {
1686
+ type: "object",
1687
+ properties: {
1688
+ site_id: {
1689
+ type: "string",
1690
+ description: "Site ID (account_id). Optional if SEALMETRICS_SITE_ID env var is set."
1691
+ },
1692
+ min_score: {
1693
+ type: "number",
1694
+ description: "Minimum suspicion score threshold (1-100, default: 50).",
1695
+ default: 50
1696
+ },
1697
+ limit: LIMIT_SCHEMA
1698
+ }
1699
+ },
1700
+ handler: async (client, args) => {
1701
+ return client.request("/bot-stats/suspicious-sessions", {
1702
+ account_id: resolveSiteId(args),
1703
+ min_score: String(args.min_score ?? 50),
1704
+ limit: String(args.limit ?? 20)
1705
+ });
1706
+ }
1707
+ };
1708
+
1709
+ // dist/tools/segments.js
1710
+ var listSegmentsTool = {
1711
+ name: "list_segments",
1712
+ description: "List all segments available for a site. Segments are saved filter sets (e.g. 'Mobile users from Spain', 'Organic traffic') that can be applied to stats queries via the segment parameter.",
1713
+ inputSchema: {
1714
+ type: "object",
1715
+ properties: {
1716
+ site_id: {
1717
+ type: "string",
1718
+ description: "Site ID (account_id). Optional if SEALMETRICS_SITE_ID env var is set."
1719
+ },
1720
+ include_system: {
1721
+ type: "boolean",
1722
+ description: "Include system segments like 'All Traffic', 'Direct' (default: true)."
1723
+ }
1724
+ }
1725
+ },
1726
+ handler: async (client, args) => {
1727
+ return client.request("/segments", {
1728
+ account_id: resolveSiteId(args),
1729
+ include_system: args.include_system != null ? String(args.include_system) : void 0
1730
+ });
1731
+ }
1732
+ };
1733
+ var getSegmentTool = {
1734
+ name: "get_segment",
1735
+ description: "Get details of a specific segment including its filter definition. The segment_id can be either the segment ID (seg_xxx) or the segment name.",
1736
+ inputSchema: {
1737
+ type: "object",
1738
+ properties: {
1739
+ site_id: {
1740
+ type: "string",
1741
+ description: "Site ID (account_id). Optional if SEALMETRICS_SITE_ID env var is set."
1742
+ },
1743
+ segment_id: {
1744
+ type: "string",
1745
+ description: "Segment ID (seg_xxx) or segment name."
1746
+ }
248
1747
  },
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
- },
1748
+ required: ["segment_id"]
1749
+ },
1750
+ handler: async (client, args) => {
1751
+ const segmentId = args.segment_id;
1752
+ return client.request(`/segments/${encodeURIComponent(segmentId)}`, {
1753
+ account_id: resolveSiteId(args)
1754
+ });
1755
+ }
1756
+ };
1757
+
1758
+ // dist/tools/alerts.js
1759
+ var listAlertsTool = {
1760
+ name: "list_alerts",
1761
+ description: "List alert rules configured for a site. Shows rule name, metric, condition, threshold, and status (active/paused). Alerts monitor metrics and trigger notifications when thresholds are crossed.",
1762
+ inputSchema: {
1763
+ type: "object",
1764
+ properties: {
1765
+ site_id: {
1766
+ type: "string",
1767
+ description: "Site ID (account_id). Optional if SEALMETRICS_SITE_ID env var is set."
1768
+ },
1769
+ include_inactive: {
1770
+ type: "boolean",
1771
+ description: "Include inactive/paused alert rules (default: false)."
1772
+ }
1773
+ }
1774
+ },
1775
+ handler: async (client, args) => {
1776
+ return client.requestDirect("/alerts/rules", {
1777
+ account_id: resolveSiteId(args),
1778
+ include_inactive: args.include_inactive != null ? String(args.include_inactive) : void 0
1779
+ });
1780
+ }
1781
+ };
1782
+ var getAlertHistoryTool = {
1783
+ name: "get_alert_history",
1784
+ description: "Get history of triggered alerts: when they fired, current status (active/resolved/acknowledged), and which rule triggered them. Useful for reviewing past incidents.",
1785
+ inputSchema: {
1786
+ type: "object",
1787
+ properties: {
1788
+ site_id: {
1789
+ type: "string",
1790
+ description: "Site ID (account_id). Optional if SEALMETRICS_SITE_ID env var is set."
1791
+ },
1792
+ rule_id: {
1793
+ type: "number",
1794
+ description: "Filter by specific alert rule ID."
1795
+ },
1796
+ status: {
1797
+ type: "string",
1798
+ description: "Filter by alert status.",
1799
+ enum: ["active", "resolved", "acknowledged"]
1800
+ },
1801
+ limit: LIMIT_SCHEMA,
1802
+ offset: {
1803
+ type: "number",
1804
+ description: "Offset for pagination (default: 0).",
1805
+ default: 0
1806
+ }
1807
+ }
1808
+ },
1809
+ handler: async (client, args) => {
1810
+ return client.requestDirect("/alerts/history", {
1811
+ account_id: resolveSiteId(args),
1812
+ rule_id: args.rule_id != null ? String(args.rule_id) : void 0,
1813
+ status: args.status,
1814
+ limit: String(args.limit ?? 50),
1815
+ offset: args.offset != null ? String(args.offset) : void 0
1816
+ });
1817
+ }
1818
+ };
1819
+ var getAlertStatsTool = {
1820
+ name: "get_alert_stats",
1821
+ description: "Get alert statistics for a site: total rules, active alerts, resolved count, and acknowledgement rate. Provides a quick health overview of the alerting system.",
1822
+ inputSchema: {
1823
+ type: "object",
1824
+ properties: {
1825
+ site_id: {
1826
+ type: "string",
1827
+ description: "Site ID (account_id). Optional if SEALMETRICS_SITE_ID env var is set."
1828
+ }
1829
+ }
1830
+ },
1831
+ handler: async (client, args) => {
1832
+ return client.requestDirect("/alerts/stats", {
1833
+ account_id: resolveSiteId(args)
1834
+ });
1835
+ }
1836
+ };
1837
+
1838
+ // dist/tools/webhooks.js
1839
+ var listWebhooksTool = {
1840
+ name: "list_webhooks",
1841
+ description: "List webhook endpoints configured for a site. Shows endpoint URL, subscribed event types, and active status. Webhooks send real-time HTTP notifications when events occur.",
1842
+ inputSchema: {
1843
+ type: "object",
1844
+ properties: {
1845
+ site_id: {
1846
+ type: "string",
1847
+ description: "Site ID (account_id). Optional if SEALMETRICS_SITE_ID env var is set."
1848
+ },
1849
+ include_inactive: {
1850
+ type: "boolean",
1851
+ description: "Include inactive webhook endpoints (default: false)."
1852
+ },
1853
+ limit: LIMIT_SCHEMA,
1854
+ offset: {
1855
+ type: "number",
1856
+ description: "Offset for pagination (default: 0).",
1857
+ default: 0
1858
+ }
1859
+ }
1860
+ },
1861
+ handler: async (client, args) => {
1862
+ return client.requestDirect("/webhooks", {
1863
+ account_id: resolveSiteId(args),
1864
+ include_inactive: args.include_inactive != null ? String(args.include_inactive) : void 0,
1865
+ limit: String(args.limit ?? 50),
1866
+ offset: args.offset != null ? String(args.offset) : void 0
1867
+ });
1868
+ }
1869
+ };
1870
+ var listWebhookDeliveriesTool = {
1871
+ name: "list_webhook_deliveries",
1872
+ description: "List delivery attempts for a specific webhook endpoint. Shows HTTP status, response time, and success/failure for each delivery. Useful for debugging webhook integration issues.",
1873
+ inputSchema: {
1874
+ type: "object",
1875
+ properties: {
1876
+ site_id: {
1877
+ type: "string",
1878
+ description: "Site ID (account_id). Optional if SEALMETRICS_SITE_ID env var is set."
1879
+ },
1880
+ endpoint_id: {
1881
+ type: "string",
1882
+ description: "Webhook endpoint UUID."
1883
+ },
1884
+ status: {
1885
+ type: "string",
1886
+ description: "Filter by delivery status.",
1887
+ enum: ["pending", "success", "failed"]
1888
+ },
1889
+ limit: LIMIT_SCHEMA,
1890
+ offset: {
1891
+ type: "number",
1892
+ description: "Offset for pagination (default: 0).",
1893
+ default: 0
1894
+ }
297
1895
  },
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
- },
1896
+ required: ["endpoint_id"]
1897
+ },
1898
+ handler: async (client, args) => {
1899
+ const endpointId = args.endpoint_id;
1900
+ return client.requestDirect(`/webhooks/${encodeURIComponent(endpointId)}/deliveries`, {
1901
+ account_id: resolveSiteId(args),
1902
+ status: args.status,
1903
+ limit: String(args.limit ?? 50),
1904
+ offset: args.offset != null ? String(args.offset) : void 0
1905
+ });
1906
+ }
1907
+ };
1908
+ var getWebhookStatsTool = {
1909
+ name: "get_webhook_stats",
1910
+ description: "Get delivery statistics for a specific webhook endpoint: total deliveries, success rate, average response time, and failure breakdown. Useful for monitoring webhook reliability.",
1911
+ inputSchema: {
1912
+ type: "object",
1913
+ properties: {
1914
+ site_id: {
1915
+ type: "string",
1916
+ description: "Site ID (account_id). Optional if SEALMETRICS_SITE_ID env var is set."
1917
+ },
1918
+ endpoint_id: {
1919
+ type: "string",
1920
+ description: "Webhook endpoint UUID."
1921
+ },
1922
+ hours: {
1923
+ type: "number",
1924
+ description: "Hours to look back for statistics (1-720, default: 24).",
1925
+ default: 24
1926
+ }
341
1927
  },
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
- },
1928
+ required: ["endpoint_id"]
1929
+ },
1930
+ handler: async (client, args) => {
1931
+ const endpointId = args.endpoint_id;
1932
+ return client.requestDirect(`/webhooks/${encodeURIComponent(endpointId)}/stats`, {
1933
+ account_id: resolveSiteId(args),
1934
+ hours: String(args.hours ?? 24)
1935
+ });
1936
+ }
1937
+ };
1938
+
1939
+ // dist/tools/tracking.js
1940
+ var JS_API_REFERENCE = {
1941
+ pageview: {
1942
+ description: "Tracked automatically on load and SPA navigation. Manual call rarely needed.",
1943
+ signatures: [
1944
+ { call: "sealmetrics()", description: "Manual pageview" },
1945
+ {
1946
+ call: "sealmetrics({ group: 'blog' })",
1947
+ description: "Pageview with content grouping"
1948
+ }
1949
+ ]
1950
+ },
1951
+ conversion: {
1952
+ description: "Track conversions with monetary value. Use for purchases, signups, leads.",
1953
+ signatures: [
1954
+ {
1955
+ call: "sealmetrics.conv('purchase', 99.99)",
1956
+ description: "Basic conversion"
1957
+ },
1958
+ {
1959
+ call: "sealmetrics.conv('purchase', 149.99, { currency: 'EUR' })",
1960
+ description: "Conversion with properties"
1961
+ },
1962
+ {
1963
+ call: "sealmetrics.conv('lead', 0, { source: 'contact_form' })",
1964
+ description: "Lead without monetary value"
1965
+ }
1966
+ ]
1967
+ },
1968
+ microconversion: {
1969
+ description: "Track funnel steps and engagement events. Use for add_to_cart, scroll milestones, video plays, newsletter signups.",
1970
+ signatures: [
1971
+ {
1972
+ call: "sealmetrics.micro('add_to_cart')",
1973
+ description: "Basic microconversion"
1974
+ },
1975
+ {
1976
+ call: "sealmetrics.micro('add_to_cart', { product_id: 'SKU-123', price: 49.99 })",
1977
+ description: "Microconversion with properties"
1978
+ }
1979
+ ]
1980
+ }
1981
+ };
1982
+ var IMPLEMENTATION_GUIDE = {
1983
+ installation: [
1984
+ "Add the <script> tag in the <head> of every page, before </head>.",
1985
+ "Use the 'defer' attribute \u2014 the script loads async and never blocks rendering.",
1986
+ "Pageviews are tracked automatically on load and SPA navigation (pushState, replaceState, popstate).",
1987
+ "No cookies, no localStorage, no consent banner needed (GDPR-friendly)."
1988
+ ],
1989
+ content_grouping: {
1990
+ description: "Categorize pages into sections for better analysis. Add via URL param or JS call.",
1991
+ via_url_param: '<script src="https://t.sealmetrics.com/t.js?id=SITE_ID&group=blog" defer></script>',
1992
+ via_js: "sealmetrics({ group: 'blog' })",
1993
+ recommended_groups: [
1994
+ "blog",
1995
+ "product",
1996
+ "category",
1997
+ "checkout",
1998
+ "docs",
1999
+ "app",
2000
+ "landing",
2001
+ "support"
2002
+ ]
2003
+ },
2004
+ naming_conventions: [
2005
+ "Use snake_case for conversion and microconversion types: 'add_to_cart', not 'addToCart'.",
2006
+ "Use descriptive names: 'begin_checkout', not 'step2'.",
2007
+ "Do NOT include order IDs or user IDs in the type name \u2014 use properties instead.",
2008
+ "Keep types stable \u2014 changing names breaks historical data continuity."
2009
+ ],
2010
+ spa_support: "Automatic. Works with React Router, Vue Router, Next.js, Nuxt, Angular, and any History API-based routing. No extra config.",
2011
+ debugging: "Add ?debug=1 to any page URL to enable console logging of all tracking events."
2012
+ };
2013
+ var EXAMPLES = {
2014
+ ecommerce: {
2015
+ description: "E-commerce site with product pages, cart, and checkout",
2016
+ code: `<!-- In <head> of all pages -->
2017
+ <script src="https://t.sealmetrics.com/t.js?id=SITE_ID&group=product" defer></script>
2018
+
2019
+ <script>
2020
+ // Add to cart button
2021
+ document.querySelector('.add-to-cart').addEventListener('click', function() {
2022
+ sealmetrics.micro('add_to_cart', {
2023
+ product_id: this.dataset.productId,
2024
+ price: parseFloat(this.dataset.price)
2025
+ });
2026
+ });
2027
+
2028
+ // Begin checkout
2029
+ document.querySelector('.checkout-btn').addEventListener('click', function() {
2030
+ sealmetrics.micro('begin_checkout', { cart_value: getCartTotal() });
2031
+ });
2032
+ </script>
2033
+
2034
+ <!-- On thank-you page (group=checkout) -->
2035
+ <script src="https://t.sealmetrics.com/t.js?id=SITE_ID&group=checkout" defer></script>
2036
+ <script>
2037
+ sealmetrics.conv('purchase', 189.99, {
2038
+ currency: 'EUR',
2039
+ payment_method: 'credit_card'
2040
+ });
2041
+ </script>`
2042
+ },
2043
+ saas: {
2044
+ description: "SaaS / lead generation site",
2045
+ code: `<!-- In <head> -->
2046
+ <script src="https://t.sealmetrics.com/t.js?id=SITE_ID&group=marketing" defer></script>
2047
+
2048
+ <script>
2049
+ // Demo request form
2050
+ document.querySelector('#demo-form').addEventListener('submit', function() {
2051
+ sealmetrics.conv('lead', 0, { form_name: 'demo_request' });
2052
+ });
2053
+
2054
+ // Newsletter signup
2055
+ document.querySelector('#newsletter').addEventListener('submit', function() {
2056
+ sealmetrics.micro('newsletter_signup', { position: 'footer' });
2057
+ });
2058
+ </script>
2059
+
2060
+ <!-- On signup success page -->
2061
+ <script>
2062
+ sealmetrics.conv('signup', 0, { plan: 'trial' });
2063
+ </script>
2064
+
2065
+ <!-- On upgrade/payment -->
2066
+ <script>
2067
+ sealmetrics.conv('purchase', 49, { plan: 'pro_monthly', currency: 'USD' });
2068
+ </script>`
2069
+ },
2070
+ blog_media: {
2071
+ description: "Blog or media site with content engagement tracking",
2072
+ code: `<!-- In <head> -->
2073
+ <script src="https://t.sealmetrics.com/t.js?id=SITE_ID&group=blog" defer></script>
2074
+
2075
+ <script>
2076
+ // Video engagement
2077
+ document.querySelector('video')?.addEventListener('play', function() {
2078
+ sealmetrics.micro('video_play', { video_id: this.dataset.videoId });
2079
+ });
2080
+ document.querySelector('video')?.addEventListener('ended', function() {
2081
+ sealmetrics.micro('video_complete', { video_id: this.dataset.videoId });
2082
+ });
2083
+
2084
+ // Scroll depth milestones
2085
+ var tracked = {};
2086
+ window.addEventListener('scroll', function() {
2087
+ var pct = Math.round(window.scrollY / (document.body.scrollHeight - window.innerHeight) * 100);
2088
+ [25, 50, 75, 100].forEach(function(m) {
2089
+ if (pct >= m && !tracked[m]) {
2090
+ tracked[m] = true;
2091
+ sealmetrics.micro('scroll_' + m);
2092
+ }
2093
+ });
2094
+ });
2095
+ </script>`
2096
+ },
2097
+ react_nextjs: {
2098
+ description: "React / Next.js application",
2099
+ code: `// In layout.tsx or _app.tsx \u2014 add the script tag
2100
+ import Script from 'next/script';
2101
+
2102
+ export default function RootLayout({ children }) {
2103
+ return (
2104
+ <html>
2105
+ <head>
2106
+ <Script
2107
+ src="https://t.sealmetrics.com/t.js?id=SITE_ID"
2108
+ strategy="afterInteractive"
2109
+ />
2110
+ </head>
2111
+ <body>{children}</body>
2112
+ </html>
2113
+ );
2114
+ }
2115
+
2116
+ // In any component \u2014 track conversions/microconversions
2117
+ function CheckoutButton({ total, currency }) {
2118
+ const handlePurchase = () => {
2119
+ window.sealmetrics?.conv('purchase', total, { currency });
2120
+ };
2121
+ return <button onClick={handlePurchase}>Complete Purchase</button>;
2122
+ }
2123
+
2124
+ // Track microconversions
2125
+ function AddToCartButton({ productId, price }) {
2126
+ const handleClick = () => {
2127
+ window.sealmetrics?.micro('add_to_cart', { product_id: productId, price });
2128
+ };
2129
+ return <button onClick={handleClick}>Add to Cart</button>;
2130
+ }`
2131
+ }
2132
+ };
2133
+ var getTrackingCodeTool = {
2134
+ name: "get_tracking_code",
2135
+ description: "Get the tracking pixel code for a site and the full JavaScript API reference for implementing pageviews, conversions, microconversions, and content grouping. Returns the site-specific <script> tag plus implementation guide with examples for e-commerce, SaaS, blog, and React/Next.js.",
2136
+ inputSchema: {
2137
+ type: "object",
2138
+ properties: {
2139
+ site_id: {
2140
+ type: "string",
2141
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
2142
+ }
2143
+ }
2144
+ },
2145
+ handler: async (client, args) => {
2146
+ const siteId = resolveSiteId(args);
2147
+ const pixel = await client.request(`/sites/${encodeURIComponent(siteId)}/pixel`);
2148
+ return {
2149
+ site_id: pixel.site_id ?? pixel.account_id,
2150
+ script_tag: pixel.script_tag,
2151
+ tracker_url: pixel.tracker_url,
2152
+ js_api: JS_API_REFERENCE,
2153
+ implementation_guide: IMPLEMENTATION_GUIDE,
2154
+ examples: EXAMPLES
2155
+ };
2156
+ }
2157
+ };
2158
+
2159
+ // dist/tools/properties.js
2160
+ var TABLE_SCHEMA = {
2161
+ type: "string",
2162
+ description: 'Table to query: "conversions", "microconversions", "both", or "conversion_items". Default: "both".',
2163
+ enum: ["conversions", "microconversions", "both", "conversion_items"],
2164
+ default: "both"
2165
+ };
2166
+ var listPropertyKeysTool = {
2167
+ name: "list_property_keys",
2168
+ description: "Get the list of available custom property keys from conversions and/or microconversions. Use this to discover what property keys exist before querying values or breakdowns.",
2169
+ inputSchema: {
2170
+ type: "object",
2171
+ properties: {
2172
+ site_id: {
2173
+ type: "string",
2174
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
2175
+ },
2176
+ period: PERIOD_SCHEMA,
2177
+ table: TABLE_SCHEMA
2178
+ }
2179
+ },
2180
+ handler: async (client, args) => {
2181
+ return client.request("/stats/properties/keys", {
2182
+ site_id: resolveSiteId(args),
2183
+ period: args.period ?? "30d",
2184
+ table: args.table ?? "both"
2185
+ });
2186
+ }
2187
+ };
2188
+ var getPropertyValuesTool = {
2189
+ name: "get_property_values",
2190
+ description: "Get property values with counts, grouped by a UTM parameter. Paginated. Use to see which values a specific property key has and how they distribute across traffic sources.",
2191
+ inputSchema: {
2192
+ type: "object",
2193
+ properties: {
2194
+ site_id: {
2195
+ type: "string",
2196
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
2197
+ },
2198
+ property_key: {
2199
+ type: "string",
2200
+ description: "The property key to analyze (e.g. 'product_name', 'plan_type')."
2201
+ },
2202
+ period: PERIOD_SCHEMA,
2203
+ group_by: {
2204
+ type: "string",
2205
+ description: 'Group results by: "utm_source", "utm_medium", "utm_campaign", or "all". Default: "utm_source".',
2206
+ enum: ["utm_source", "utm_medium", "utm_campaign", "all"],
2207
+ default: "utm_source"
2208
+ },
2209
+ table: TABLE_SCHEMA,
2210
+ conversion_type: {
2211
+ type: "string",
2212
+ description: "Filter by conversion type."
2213
+ },
2214
+ limit: LIMIT_SCHEMA,
2215
+ page: PAGE_SCHEMA
385
2216
  },
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
- },
2217
+ required: ["property_key"]
2218
+ },
2219
+ handler: async (client, args) => {
2220
+ return client.requestPaginated("/stats/properties/values", {
2221
+ site_id: resolveSiteId(args),
2222
+ property_key: args.property_key,
2223
+ period: args.period ?? "30d",
2224
+ group_by: args.group_by ?? "utm_source",
2225
+ table: args.table ?? "both",
2226
+ conversion_type: args.conversion_type,
2227
+ page_size: String(args.limit ?? 50),
2228
+ page: args.page != null ? String(args.page) : void 0
2229
+ });
2230
+ }
2231
+ };
2232
+ var getPropertyBreakdownTool = {
2233
+ name: "get_property_breakdown",
2234
+ description: "Get a complete property breakdown with pivot-table style data. Shows all values for a property key with their counts and revenue \u2014 ideal for analyzing product or category performance.",
2235
+ inputSchema: {
2236
+ type: "object",
2237
+ properties: {
2238
+ site_id: {
2239
+ type: "string",
2240
+ description: "Site ID. Optional if SEALMETRICS_SITE_ID env var is set."
2241
+ },
2242
+ property_key: {
2243
+ type: "string",
2244
+ description: "The property key to analyze (e.g. 'product_name', 'plan_type')."
2245
+ },
2246
+ period: PERIOD_SCHEMA,
2247
+ table: TABLE_SCHEMA,
2248
+ conversion_type: {
2249
+ type: "string",
2250
+ description: "Filter by conversion type."
2251
+ }
408
2252
  },
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
- },
2253
+ required: ["property_key"]
2254
+ },
2255
+ handler: async (client, args) => {
2256
+ return client.request("/stats/properties/breakdown", {
2257
+ site_id: resolveSiteId(args),
2258
+ property_key: args.property_key,
2259
+ period: args.period ?? "30d",
2260
+ table: args.table ?? "both",
2261
+ conversion_type: args.conversion_type
2262
+ });
2263
+ }
2264
+ };
2265
+
2266
+ // dist/tools/index.js
2267
+ var ALL_TOOLS = [
2268
+ // Sites
2269
+ listSitesTool,
2270
+ getSiteTool,
2271
+ // Overview
2272
+ getOverviewTool,
2273
+ // Traffic
2274
+ getTrafficSourcesTool,
2275
+ getTrafficMediumsTool,
2276
+ getCampaignsTool,
2277
+ getTermsTool,
2278
+ getTopSourcesTool,
2279
+ getTopCampaignsTool,
2280
+ getTopTermsTool,
2281
+ getTopReferrersTool,
2282
+ // Pages & Content
2283
+ getPagesTool,
2284
+ getLandingPagesTool,
2285
+ getTopPagesTool,
2286
+ getTopLandingPagesTool,
2287
+ getLandingPagesByContentGroupTool,
2288
+ getContentGroupsTool,
2289
+ // Conversions
2290
+ getConversionsTool,
2291
+ getMicroconversionsTool,
2292
+ listMicroconversionTypesTool,
2293
+ getMicroconversionDetailsTool,
2294
+ // Conversions — raw event-level (PRD 08)
2295
+ getConversionsRawTool,
2296
+ getMicroconversionsRawTool,
2297
+ getConversionItemsRawTool,
2298
+ // Audience
2299
+ getCountriesTool,
2300
+ getDevicesTool,
2301
+ getDeviceTypesTool,
2302
+ getBrowsersTool,
2303
+ getOperatingSystemsTool,
2304
+ // Channels
2305
+ getChannelsTool,
2306
+ getTopChannelsTool,
2307
+ listChannelRulesTool,
2308
+ // Properties
2309
+ listPropertyKeysTool,
2310
+ getPropertyValuesTool,
2311
+ getPropertyBreakdownTool,
2312
+ // Funnel
2313
+ getFunnelTool,
2314
+ // Bot Detection
2315
+ getBotStatsTool,
2316
+ getSuspiciousSessionsTool,
2317
+ // Segments
2318
+ listSegmentsTool,
2319
+ getSegmentTool,
2320
+ // Alerts
2321
+ listAlertsTool,
2322
+ getAlertHistoryTool,
2323
+ getAlertStatsTool,
2324
+ // Webhooks
2325
+ listWebhooksTool,
2326
+ listWebhookDeliveriesTool,
2327
+ getWebhookStatsTool,
2328
+ // Tracking
2329
+ getTrackingCodeTool
2330
+ ];
2331
+
2332
+ // ../setup-core/dist/detect/index.js
2333
+ import { existsSync as existsSync2, readFileSync, readdirSync } from "node:fs";
2334
+ import { join as join2 } from "node:path";
2335
+
2336
+ // ../setup-core/dist/detect/cms.js
2337
+ import { existsSync } from "node:fs";
2338
+ import { join } from "node:path";
2339
+ function detectPlatform(cwd) {
2340
+ const has = (...rel) => rel.some((r) => existsSync(join(cwd, r)));
2341
+ if (has("wp-config.php", "wp-content")) {
2342
+ if (has("wp-content/plugins/woocommerce", "wp-content/plugins/woocommerce/woocommerce.php")) {
2343
+ return "woocommerce";
2344
+ }
2345
+ return "wordpress";
2346
+ }
2347
+ if (has("config/settings.inc.php", "app/config/parameters.php") && has("prestashop", "classes/PrestaShopAutoload.php")) {
2348
+ return "prestashop";
2349
+ }
2350
+ if (has("classes/PrestaShopAutoload.php"))
2351
+ return "prestashop";
2352
+ if (has("bin/magento", "app/etc/env.php") && has("app/etc/di.xml", "vendor/magento")) {
2353
+ return "magento2";
2354
+ }
2355
+ if (has("bin/magento"))
2356
+ return "magento2";
2357
+ if (has("core/lib/Drupal.php"))
2358
+ return "drupal";
2359
+ if (has("configuration.php") && has("libraries/joomla", "administrator/manifests")) {
2360
+ return "joomla";
2361
+ }
2362
+ if (has("system/startup.php") && has("catalog/controller"))
2363
+ return "opencart";
2364
+ return void 0;
2365
+ }
2366
+
2367
+ // ../setup-core/dist/detect/index.js
2368
+ var LOCATIONS = {
2369
+ "next-app": {
2370
+ location: "app/layout.tsx",
2371
+ hint: "inside <head>, or via a next/script <Script> component",
2372
+ strategy: "next-app-router-layout"
2373
+ },
2374
+ "next-pages": {
2375
+ location: "pages/_document.tsx",
2376
+ hint: "inside <Head> in _document (or pages/_app.tsx)",
2377
+ strategy: "next-pages-document"
2378
+ },
2379
+ astro: {
2380
+ location: "src/layouts/Layout.astro",
2381
+ hint: "inside <head> of your root layout",
2382
+ strategy: "astro-layout-head"
2383
+ },
2384
+ remix: {
2385
+ location: "app/root.tsx",
2386
+ hint: "inside the <head> region of the root route",
2387
+ strategy: "remix-root-head"
2388
+ },
2389
+ nuxt: {
2390
+ location: "nuxt.config.ts",
2391
+ hint: "in app.head.script (or app.vue <head>)",
2392
+ strategy: "nuxt-config-head"
2393
+ },
2394
+ sveltekit: {
2395
+ location: "src/app.html",
2396
+ hint: "inside <head>, above %sveltekit.head%",
2397
+ strategy: "sveltekit-app-html"
2398
+ },
2399
+ vite: {
2400
+ location: "index.html",
2401
+ hint: "just before </head>",
2402
+ strategy: "vite-index-html"
2403
+ },
2404
+ html: {
2405
+ location: "index.html",
2406
+ hint: "just before </head>",
2407
+ strategy: "static-html-head"
2408
+ },
2409
+ unknown: {
2410
+ location: "your site's root HTML / <head>",
2411
+ hint: "just before the closing </head> tag on every page",
2412
+ strategy: "manual"
2413
+ }
2414
+ };
2415
+ function detect(cwd, opts) {
2416
+ if (opts?.noInject) {
2417
+ return makeDetection("unknown", { provisionOnly: true });
2418
+ }
2419
+ const platform = detectPlatform(cwd);
2420
+ if (platform) {
2421
+ return makeDetection("unknown", { platform, provisionOnly: false });
2422
+ }
2423
+ const pkg = readPackageJson(cwd);
2424
+ if (pkg) {
2425
+ const fw = detectFromPackageJson(cwd, pkg);
2426
+ return makeDetection(fw, { provisionOnly: false });
2427
+ }
2428
+ if (findIndexHtml(cwd)) {
2429
+ return makeDetection("html", { provisionOnly: false });
2430
+ }
2431
+ if (isEmptyish(cwd)) {
2432
+ return makeDetection("unknown", { provisionOnly: true });
2433
+ }
2434
+ return makeDetection("unknown", { provisionOnly: false });
2435
+ }
2436
+ var detectFramework = detect;
2437
+ function detectFromPackageJson(cwd, pkg) {
2438
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
2439
+ const has = (name) => Object.prototype.hasOwnProperty.call(deps, name);
2440
+ if (has("next")) {
2441
+ return hasAppRouter(cwd) ? "next-app" : "next-pages";
2442
+ }
2443
+ if (has("astro"))
2444
+ return "astro";
2445
+ if (has("@remix-run/react") || has("@remix-run/node") || has("@remix-run/serve"))
2446
+ return "remix";
2447
+ if (has("nuxt") || has("nuxt3") || has("nuxt-edge"))
2448
+ return "nuxt";
2449
+ if (has("@sveltejs/kit"))
2450
+ return "sveltekit";
2451
+ if (has("vite"))
2452
+ return "vite";
2453
+ if (findIndexHtml(cwd))
2454
+ return "html";
2455
+ return "unknown";
2456
+ }
2457
+ function hasAppRouter(cwd) {
2458
+ const appDirs = ["app", "src/app"];
2459
+ const pagesDirs = ["pages", "src/pages"];
2460
+ const hasApp = appDirs.some((d) => existsSync2(join2(cwd, d)));
2461
+ const hasPages = pagesDirs.some((d) => existsSync2(join2(cwd, d)));
2462
+ if (hasApp)
2463
+ return true;
2464
+ if (hasPages)
2465
+ return false;
2466
+ return true;
2467
+ }
2468
+ function readPackageJson(cwd) {
2469
+ const p = join2(cwd, "package.json");
2470
+ if (!existsSync2(p))
2471
+ return null;
2472
+ try {
2473
+ return JSON.parse(readFileSync(p, "utf8"));
2474
+ } catch {
2475
+ return null;
2476
+ }
2477
+ }
2478
+ function findIndexHtml(cwd) {
2479
+ return ["index.html", "public/index.html", "src/index.html"].some((f) => existsSync2(join2(cwd, f)));
2480
+ }
2481
+ function isEmptyish(cwd) {
2482
+ try {
2483
+ const entries = readdirSync(cwd).filter((e) => !e.startsWith(".") && e !== "seal.config.json");
2484
+ return entries.length === 0;
2485
+ } catch {
2486
+ return true;
2487
+ }
2488
+ }
2489
+ function makeDetection(framework, extra) {
2490
+ const loc = LOCATIONS[framework];
2491
+ const detection = {
2492
+ framework,
2493
+ strategy: extra.platform ? `cms-plugin-${extra.platform}` : loc.strategy,
2494
+ recommendedLocation: loc.location,
2495
+ placementHint: loc.hint,
2496
+ provisionOnly: extra.provisionOnly
2497
+ };
2498
+ if (extra.platform)
2499
+ detection.platform = extra.platform;
2500
+ return detection;
2501
+ }
2502
+
2503
+ // ../setup-core/dist/provision.js
2504
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
2505
+ var ProvisionError = class extends Error {
2506
+ code;
2507
+ httpStatus;
2508
+ retryAfter;
2509
+ detail;
2510
+ constructor(code, message, opts) {
2511
+ super(message);
2512
+ this.name = "ProvisionError";
2513
+ this.code = code;
2514
+ this.httpStatus = opts?.httpStatus;
2515
+ this.retryAfter = opts?.retryAfter;
2516
+ this.detail = opts?.detail;
2517
+ }
2518
+ };
2519
+ var PixelStatusError = class extends Error {
2520
+ httpStatus;
2521
+ detail;
2522
+ constructor(message, opts) {
2523
+ super(message);
2524
+ this.name = "PixelStatusError";
2525
+ this.httpStatus = opts?.httpStatus;
2526
+ this.detail = opts?.detail;
2527
+ }
2528
+ };
2529
+ function normalizeBaseUrl(baseUrl) {
2530
+ return baseUrl.replace(/\/+$/, "");
2531
+ }
2532
+ function buildProvisionBody(input) {
2533
+ return {
2534
+ site_name: input.siteName,
2535
+ domain: input.domain,
2536
+ email: input.email,
2537
+ name: input.name,
2538
+ accept_terms: true,
2539
+ install_source: input.installSource,
2540
+ timezone: input.timezone
2541
+ };
2542
+ }
2543
+ function provisionErrorForStatus(status, opts) {
2544
+ if (status === 401) {
2545
+ return new ProvisionError("AUTH_REQUIRED", "The provision key was rejected (invalid or revoked).", {
2546
+ httpStatus: 401
2547
+ });
2548
+ }
2549
+ if (status === 409) {
2550
+ return new ProvisionError("EMAIL_EXISTS", "An account already exists for this email.", {
2551
+ httpStatus: 409
2552
+ });
2553
+ }
2554
+ if (status === 429) {
2555
+ return new ProvisionError("RATE_LIMITED", "Provisioning is rate-limited. Please wait and retry.", {
2556
+ httpStatus: 429,
2557
+ retryAfter: opts?.retryAfter
2558
+ });
2559
+ }
2560
+ if (status === 503) {
2561
+ return new ProvisionError("PROVISIONING_DISABLED", "Provisioning is currently disabled on the server.", {
2562
+ httpStatus: 503
2563
+ });
2564
+ }
2565
+ return new ProvisionError("BACKEND_ERROR", `Provisioning failed (HTTP ${status}).`, {
2566
+ httpStatus: status,
2567
+ detail: opts?.detail
2568
+ });
2569
+ }
2570
+ function unwrapProvisionData(json) {
2571
+ const data = json?.data;
2572
+ if (!data || !data.account_id || !data.api_key) {
2573
+ throw new ProvisionError("BAD_RESPONSE", "Provisioning returned an unexpected response.", {
2574
+ httpStatus: 200
2575
+ });
2576
+ }
2577
+ return data;
2578
+ }
2579
+ async function fetchWithTimeout(fetchImpl, url, init, timeoutMs) {
2580
+ const controller = new AbortController();
2581
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2582
+ try {
2583
+ return await fetchImpl(url, { ...init, signal: controller.signal });
2584
+ } finally {
2585
+ clearTimeout(timeout);
2586
+ }
2587
+ }
2588
+ async function safeText(res) {
2589
+ try {
2590
+ return await res.text();
2591
+ } catch {
2592
+ return "";
2593
+ }
2594
+ }
2595
+ async function fetchPixelStatus(accountId, options) {
2596
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
2597
+ const fetchImpl = options.fetchImpl ?? fetch;
2598
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
2599
+ const url = `${baseUrl}/sites/${encodeURIComponent(accountId)}/pixel/status`;
2600
+ let res;
2601
+ try {
2602
+ res = await fetchWithTimeout(fetchImpl, url, { method: "GET", headers: { "X-API-Key": options.apiKey, Accept: "application/json" } }, timeoutMs);
2603
+ } catch (e) {
2604
+ throw new PixelStatusError("Could not reach the SealMetrics API.", {
2605
+ detail: e instanceof Error ? e.message : void 0
2606
+ });
2607
+ }
2608
+ if (res.status !== 200) {
2609
+ const detail = await safeText(res);
2610
+ throw new PixelStatusError(`Pixel status check failed (HTTP ${res.status}).`, {
2611
+ httpStatus: res.status,
2612
+ detail: detail.slice(0, 200) || void 0
2613
+ });
2614
+ }
2615
+ const text = await res.text();
2616
+ let json;
2617
+ try {
2618
+ json = JSON.parse(text);
2619
+ } catch {
2620
+ throw new PixelStatusError("Pixel status returned invalid JSON.");
2621
+ }
2622
+ const data = json.data;
2623
+ if (!data || typeof data.installed !== "boolean") {
2624
+ throw new PixelStatusError("Pixel status returned an unexpected response.");
2625
+ }
2626
+ return data;
2627
+ }
2628
+
2629
+ // ../setup-core/dist/verify.js
2630
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
2631
+ async function pollPixelStatus(opts) {
2632
+ if (opts.timeoutMs <= 0) {
2633
+ return { verified: null };
2634
+ }
2635
+ const now = opts.now ?? (() => Date.now());
2636
+ const sleep3 = opts.sleep ?? defaultSleep;
2637
+ const deadline = now() + opts.timeoutMs;
2638
+ let interval = opts.intervalMs ?? 3e3;
2639
+ const maxInterval = opts.maxIntervalMs ?? 1e4;
2640
+ for (; ; ) {
2641
+ let installed = false;
2642
+ let totalHits = 0;
2643
+ try {
2644
+ const status = await opts.fetchStatus();
2645
+ installed = status.installed;
2646
+ totalHits = status.total_hits;
2647
+ } catch (e) {
2648
+ opts.onError?.(e);
2649
+ }
2650
+ if (installed) {
2651
+ return { verified: true, totalHits };
2652
+ }
2653
+ if (now() + interval >= deadline) {
2654
+ return { verified: false, totalHits };
2655
+ }
2656
+ await sleep3(interval);
2657
+ interval = Math.min(Math.floor(interval * 1.5), maxInterval);
2658
+ }
2659
+ }
2660
+
2661
+ // ../setup-core/dist/generated/guide.js
2662
+ var INSTRUMENTATION_GUIDE = "# SealMetrics Implementation Prompt for AI Assistants\n\nUse this prompt with Lovable, Cursor, Claude, or any AI assistant to implement SealMetrics tracking in your project.\n\n---\n\n## Prompt to copy:\n\n```\nI need you to implement SealMetrics analytics tracking in my project. SealMetrics is a privacy-first, cookieless analytics platform.\n\n## Account Configuration\n- Account ID: [YOUR_ACCOUNT_ID]\n- Pixel URL: https://t.sealmetrics.com\n\n## Base Tracker Installation\n\nAdd this script to the <head> of ALL pages:\n\n```html\n<script src=\"https://t.sealmetrics.com/t.js?id=[YOUR_ACCOUNT_ID]\" defer></script>\n```\n\nFor content grouping, add the `group` parameter:\n```html\n<script src=\"https://t.sealmetrics.com/t.js?id=[YOUR_ACCOUNT_ID]&group=home\" defer></script>\n```\n\n## Pages to Track with Content Groups\n\nImplement these content groups based on page type:\n\n| Page Type | Content Group | Example URL |\n|-----------|---------------|-------------|\n| Home page | `home` | `/`, `/home` |\n| Product pages | `product` | `/products/*, /product/*` |\n| Category/Catalog | `catalog` | `/category/*, /shop/*` |\n| Blog posts | `blog` | `/blog/*, /articles/*` |\n| Blog index | `blog-index` | `/blog`, `/articles` |\n| About page | `about` | `/about`, `/about-us` |\n| Contact page | `contact` | `/contact` |\n| Pricing page | `pricing` | `/pricing`, `/plans` |\n| Services | `service` | `/services/*` |\n| Portfolio/Work | `portfolio` | `/portfolio/*, /work/*` |\n| Cart | `cart` | `/cart` |\n| Checkout | `checkout` | `/checkout` |\n| Thank you/Success | `thankyou` | `/thank-you`, `/order-confirmation` |\n| Account/Dashboard | `account` | `/account/*, /dashboard/*` |\n| Legal pages | `legal` | `/privacy`, `/terms` |\n| 404 page | `404` | (any 404) |\n| Search results | `search` | `/search` |\n\n## Events to Track\n\n### 1. Microconversions (Engagement Events)\n\nUse `sealmetrics.micro(eventName, properties)` for:\n\n```javascript\n// Form submissions (contact forms)\nsealmetrics.micro('form_submit', {\n form_name: 'contact_form',\n form_type: 'contact'\n});\n\n// Newsletter signups\nsealmetrics.micro('newsletter_signup', {\n form_location: 'footer' // or 'popup', 'hero', 'sidebar'\n});\n\n// Add to cart (e-commerce)\nsealmetrics.micro('add_to_cart', {\n product_name: 'Product Name',\n product_id: '123',\n price: '99.99',\n currency: 'EUR',\n quantity: '1'\n});\n\n// Product views (e-commerce)\nsealmetrics.micro('view_item', {\n product_name: 'Product Name',\n product_id: '123',\n sku: 'SKU-123',\n price: '99.99',\n currency: 'EUR',\n category: 'Category Name',\n brand: 'Brand Name'\n});\n\n// Begin checkout\nsealmetrics.micro('begin_checkout', {\n cart_total: '199.99',\n currency: 'EUR',\n items_count: '3'\n});\n\n// CTA button clicks\nsealmetrics.micro('cta_click', {\n button_text: 'Get Started',\n button_location: 'hero'\n});\n\n// Video engagement\nsealmetrics.micro('video_play', { video_id: 'intro' });\nsealmetrics.micro('video_complete', { video_id: 'intro' });\n\n// Scroll depth\nsealmetrics.micro('scroll_50'); // 50% scroll\nsealmetrics.micro('scroll_100'); // 100% scroll\n\n// File downloads\nsealmetrics.micro('file_download', {\n file_name: 'brochure.pdf'\n});\n\n// Search\nsealmetrics.micro('search', {\n query: 'search term',\n results: '15'\n});\n\n// 404 errors\nsealmetrics.micro('404_error', {\n url: '/broken-link'\n});\n```\n\n### 2. Conversions (Revenue Events)\n\nUse `sealmetrics.conv(eventName, value, properties)` for:\n\n```javascript\n// Purchase (e-commerce)\nsealmetrics.conv('purchase', 149.99, {\n currency: 'EUR',\n payment_method: 'credit_card',\n items: [\n {\n product_name: 'Product 1',\n product_id: '123',\n price: '99.99',\n quantity: '1',\n category: 'Category',\n brand: 'Brand'\n }\n ]\n});\n\n// Lead generation (contact form that generates business)\nsealmetrics.conv('lead', 0, {\n source: 'contact_form',\n form_name: 'request_quote'\n});\n\n// Signup/Registration\nsealmetrics.conv('signup', 0, {\n plan: 'free' // or 'trial', 'premium'\n});\n\n// Subscription\nsealmetrics.conv('subscription', 29.99, {\n plan: 'pro_monthly',\n currency: 'EUR'\n});\n\n// Booking/Appointment\nsealmetrics.conv('booking', 0, {\n service: 'consultation',\n date: '2025-01-15'\n});\n```\n\n## Implementation by Business Type\n\n### SaaS / Software\n```javascript\n// Pricing page view\n// group=pricing\n\n// Free trial signup\nsealmetrics.conv('signup', 0, { plan: 'trial' });\n\n// Paid subscription\nsealmetrics.conv('subscription', 49, {\n plan: 'pro_monthly',\n currency: 'USD'\n});\n\n// Feature usage (optional)\nsealmetrics.micro('feature_use', { feature: 'export' });\n```\n\n### E-commerce\n```javascript\n// Product view\nsealmetrics.micro('view_item', { /* product data */ });\n\n// Add to cart\nsealmetrics.micro('add_to_cart', { /* product data */ });\n\n// Begin checkout\nsealmetrics.micro('begin_checkout', { /* cart data */ });\n\n// Purchase\nsealmetrics.conv('purchase', total, { /* order data */ });\n```\n\n### Lead Generation / Services\n```javascript\n// Contact form submission\nsealmetrics.conv('lead', 0, {\n source: 'contact_form',\n service_interest: 'consulting'\n});\n\n// Quote request\nsealmetrics.conv('lead', 0, {\n source: 'quote_form',\n project_type: 'website_redesign'\n});\n\n// Newsletter (not a lead, just engagement)\nsealmetrics.micro('newsletter_signup', {\n form_location: 'footer'\n});\n```\n\n### Blog / Content\n```javascript\n// Article engagement\nsealmetrics.micro('article_read', {\n article_title: 'Article Title',\n category: 'Marketing'\n});\n\n// Newsletter signup\nsealmetrics.micro('newsletter_signup');\n\n// Content download\nsealmetrics.micro('file_download', {\n file_name: 'ebook.pdf',\n file_type: 'ebook'\n});\n```\n\n### Booking / Appointments\n```javascript\n// Booking completed\nsealmetrics.conv('booking', 0, {\n service: 'haircut',\n date: '2025-01-20',\n time: '14:00'\n});\n\n// Or with value\nsealmetrics.conv('booking', 50, {\n service: 'consultation',\n currency: 'EUR'\n});\n```\n\n## Important Rules\n\n1. **NEVER include personal data**: No names, emails, phone numbers, addresses\n2. **NEVER include order IDs**: No transaction IDs, order numbers, invoice numbers\n3. **NEVER include user IDs**: No customer IDs, user identifiers\n4. **DO include**: Event types, product names, categories, prices, currencies, counts\n5. **Privacy first**: SealMetrics is cookieless and GDPR compliant by design\n\n## Form Tracking Helper\n\nFor tracking forms, use this pattern:\n\n```javascript\ndocument.querySelectorAll('form').forEach(function(form) {\n form.addEventListener('submit', function(e) {\n var formName = form.id || form.dataset.name || 'unknown_form';\n var isNewsletter = form.classList.contains('newsletter') ||\n form.querySelector('input[name*=\"newsletter\"]') ||\n formName.toLowerCase().includes('newsletter');\n\n if (isNewsletter) {\n sealmetrics.micro('newsletter_signup', {\n form_location: form.dataset.location || 'page'\n });\n } else {\n // Contact/lead form - use conversion\n sealmetrics.conv('lead', 0, {\n source: 'contact_form',\n form_name: formName\n });\n }\n });\n});\n```\n\n## Framework-Specific Implementation\n\n### React/Next.js\n```jsx\n// In layout or _app\nimport Script from 'next/script';\n\n<Script\n src=\"https://t.sealmetrics.com/t.js?id=[YOUR_ACCOUNT_ID]\"\n strategy=\"afterInteractive\"\n/>\n\n// Track events\nconst trackEvent = () => {\n if (typeof sealmetrics !== 'undefined') {\n sealmetrics.micro('event_name', { prop: 'value' });\n }\n};\n```\n\n### Vue/Nuxt\n```javascript\n// In nuxt.config.js or main.js\nhead: {\n script: [\n {\n src: 'https://t.sealmetrics.com/t.js?id=[YOUR_ACCOUNT_ID]',\n defer: true\n }\n ]\n}\n\n// Track events\nmethods: {\n trackEvent() {\n if (typeof sealmetrics !== 'undefined') {\n sealmetrics.micro('event_name', { prop: 'value' });\n }\n }\n}\n```\n\n### Plain HTML/JavaScript\n```html\n<script src=\"https://t.sealmetrics.com/t.js?id=[YOUR_ACCOUNT_ID]\" defer></script>\n\n<script>\ndocument.addEventListener('DOMContentLoaded', function() {\n // Your tracking code here\n});\n</script>\n```\n\nPlease implement these tracking calls in the appropriate places in my codebase, ensuring all business-critical user actions are tracked while maintaining user privacy.\n```\n\n---\n\n## Quick Reference Card\n\n### Content Groups\n- `home`, `product`, `catalog`, `blog`, `about`, `contact`, `pricing`, `service`, `portfolio`, `cart`, `checkout`, `thankyou`, `account`, `legal`, `404`, `search`\n\n### Microconversions (sealmetrics.micro)\n- `view_item`, `add_to_cart`, `begin_checkout`, `form_submit`, `newsletter_signup`, `cta_click`, `video_play`, `video_complete`, `scroll_50`, `scroll_100`, `file_download`, `search`, `404_error`\n\n### Conversions (sealmetrics.conv)\n- `purchase`, `lead`, `signup`, `subscription`, `booking`\n\n### Never Track\n- Order IDs, User IDs, Email addresses, Phone numbers, Personal names, Transaction IDs\n";
2663
+
2664
+ // ../setup-core/dist/guide.js
2665
+ var PRIVACY_BANNER = `> \u26A0\uFE0F **PRIVACY \u2014 READ FIRST.** Never pass personal data (names, emails, phones,
2666
+ > addresses), order/transaction/invoice IDs, or user/customer IDs to any event.
2667
+ > Track only event types, product names, categories, prices, currencies and
2668
+ > counts. SealMetrics is cookieless and GDPR-compliant by design.
2669
+
2670
+ `;
2671
+ function buildInstrumentationMarkdown(accountId) {
2672
+ const substituted = INSTRUMENTATION_GUIDE.split("[YOUR_ACCOUNT_ID]").join(accountId);
2673
+ return `# SealMetrics event instrumentation guide
2674
+
2675
+ Account ID: \`${accountId}\`
2676
+
2677
+ ` + PRIVACY_BANNER + `This guide is the authoritative API + taxonomy for instrumenting business
2678
+ events. The CLI does **not** write these calls for you (that is a separate,
2679
+ optional step); use this when you ask your agent to add conversions/events.
2680
+
2681
+ ---
2682
+
2683
+ ` + substituted;
2684
+ }
2685
+ var getInstrumentationGuide = buildInstrumentationMarkdown;
2686
+
2687
+ // ../setup-core/dist/instrument.js
2688
+ var CONV_TYPES = ["purchase", "lead", "signup", "subscription", "booking"];
2689
+ var MICRO_TYPES = [
2690
+ "view_item",
2691
+ "add_to_cart",
2692
+ "begin_checkout",
2693
+ "form_submit",
2694
+ "newsletter_signup",
2695
+ "cta_click",
2696
+ "video_play",
2697
+ "video_complete",
2698
+ "scroll_50",
2699
+ "scroll_100",
2700
+ "file_download",
2701
+ "search",
2702
+ "404_error",
2703
+ "article_read",
2704
+ "feature_use"
2705
+ ];
2706
+ function validateEventName(kind, name) {
2707
+ const known = kind === "conv" ? CONV_TYPES : MICRO_TYPES;
2708
+ const normalized = String(name ?? "").trim().toLowerCase();
2709
+ if (known.includes(normalized)) {
2710
+ return { valid: true };
2711
+ }
2712
+ return { valid: false, suggestion: closest(normalized, known) };
2713
+ }
2714
+ function closest(name, candidates) {
2715
+ let best;
2716
+ let bestScore = Infinity;
2717
+ for (const c of candidates) {
2718
+ const d = levenshtein(name, c);
2719
+ if (d < bestScore) {
2720
+ bestScore = d;
2721
+ best = c;
2722
+ }
2723
+ }
2724
+ return best !== void 0 && bestScore <= Math.max(3, Math.floor(name.length / 2)) ? best : void 0;
2725
+ }
2726
+ function levenshtein(a, b) {
2727
+ const m = a.length;
2728
+ const n = b.length;
2729
+ if (m === 0)
2730
+ return n;
2731
+ if (n === 0)
2732
+ return m;
2733
+ const row = Array.from({ length: n + 1 }, (_, i) => i);
2734
+ for (let i = 1; i <= m; i++) {
2735
+ let prev = row[0];
2736
+ row[0] = i;
2737
+ for (let j = 1; j <= n; j++) {
2738
+ const tmp = row[j];
2739
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
2740
+ prev = tmp;
2741
+ }
2742
+ }
2743
+ return row[n];
2744
+ }
2745
+ var FORBIDDEN_KEY_PATTERNS = [
2746
+ /(^|_)e?mail($|_)/i,
2747
+ /(^|_)phone($|_)/i,
2748
+ /(^|_)tel($|_)/i,
2749
+ /^name$/i,
2750
+ // a bare `name` is almost always a person's name
2751
+ /(^|_)(first|last|full|given|sur|customer|user|client|member|contact|company)_?name($|_)/i,
2752
+ /(^|_)address($|_)/i,
2753
+ // ip_address, billing_address, email_address…
2754
+ /(^|_)(order|transaction|invoice|receipt)_?(id|no|number|num)($|_)/i,
2755
+ // id/number REQUIRED
2756
+ /(^|_)(user|customer|client|member|account)_?id($|_)/i,
2757
+ /\buid\b/i,
2758
+ /(^|_)ssn($|_)/i,
2759
+ /(^|_)dob($|_)/i,
2760
+ /(^|_)ip($|_)/i
2761
+ ];
2762
+ var EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i;
2763
+ var PHONE_RE = /(?:\+?\d[\s\-().]?){9,}/;
2764
+ function detectPII(properties) {
2765
+ if (!properties || typeof properties !== "object")
2766
+ return [];
2767
+ const findings = [];
2768
+ for (const [key, value] of Object.entries(properties)) {
2769
+ if (FORBIDDEN_KEY_PATTERNS.some((re) => re.test(key))) {
2770
+ findings.push({ key, reason: "forbidden_key" });
2771
+ continue;
2772
+ }
2773
+ if (typeof value === "string") {
2774
+ if (EMAIL_RE.test(value)) {
2775
+ findings.push({ key, reason: "email_value" });
2776
+ } else if (PHONE_RE.test(value)) {
2777
+ findings.push({ key, reason: "phone_value" });
2778
+ }
2779
+ }
2780
+ }
2781
+ return findings;
2782
+ }
2783
+ function checkInstrumentation(kind, name, properties) {
2784
+ const taxonomy = validateEventName(kind, name);
2785
+ const pii = detectPII(properties);
2786
+ return { ok: taxonomy.valid && pii.length === 0, taxonomy, pii };
2787
+ }
2788
+
2789
+ // ../setup-core/dist/stack-guide.js
2790
+ var COMMON_NOTES = [
2791
+ "Load the snippet returned by POST /provision as-is \u2014 it is already async/defer-safe; do not block rendering.",
2792
+ "Place it once, as high in <head> as possible, so it loads on every page (not per-route).",
2793
+ "SPA / client-side routing: the tracker auto-tracks History API navigations (pushState/replaceState) \u2014 you do NOT need to fire a manual pageview on route change. Verify a route change produces a new pageview before adding any manual call.",
2794
+ "Content grouping: add the `group` parameter (or call the AUTO mode documented in docs/TRACKER.md) to bucket pages; never hardcode per-page groups in the snippet itself."
2795
+ ];
2796
+ var STACK_GUIDES = {
2797
+ "next-app": {
2798
+ framework: "next-app",
2799
+ location: "app/layout.tsx",
2800
+ placement: 'inside <head> of the root layout, or via a next/script <Script strategy="afterInteractive"> component',
2801
+ notes: [
2802
+ "App Router: put the snippet in the root app/layout.tsx so it covers every route segment.",
2803
+ 'Prefer next/script with strategy="afterInteractive" for the external script tag; keep the inline init in <head>.',
2804
+ "Do NOT place it in a single page.tsx \u2014 that would only load on that route.",
2805
+ ...COMMON_NOTES
2806
+ ]
2807
+ },
2808
+ "next-pages": {
2809
+ framework: "next-pages",
2810
+ location: "pages/_document.tsx",
2811
+ placement: "inside <Head> in _document.tsx (covers every page), or pages/_app.tsx with next/script",
2812
+ notes: [
2813
+ "Pages Router: _document.tsx renders once per page on the server \u2014 the snippet belongs in its <Head>.",
2814
+ "Alternatively use next/script in pages/_app.tsx so it mounts on every route.",
2815
+ ...COMMON_NOTES
2816
+ ]
2817
+ },
2818
+ astro: {
2819
+ framework: "astro",
2820
+ location: "src/layouts/Layout.astro",
2821
+ placement: "inside <head> of your root layout component",
2822
+ notes: [
2823
+ "Add it to the shared root layout that every page imports, not to individual .astro pages.",
2824
+ "Astro ships zero JS by default \u2014 the snippet is plain <script>, so it runs as written.",
2825
+ ...COMMON_NOTES
2826
+ ]
2827
+ },
2828
+ remix: {
2829
+ framework: "remix",
2830
+ location: "app/root.tsx",
2831
+ placement: "inside the <head> region of the root route (the <Links/>/<Meta/> area)",
2832
+ notes: [
2833
+ "app/root.tsx wraps every route \u2014 place the snippet in its <head> so it loads globally.",
2834
+ ...COMMON_NOTES
2835
+ ]
2836
+ },
2837
+ nuxt: {
2838
+ framework: "nuxt",
2839
+ location: "nuxt.config.ts",
2840
+ placement: "in app.head.script (or a <head> block in app.vue)",
2841
+ notes: [
2842
+ "Use app.head.script in nuxt.config.ts so Nuxt injects the tag on every page.",
2843
+ "Keep `src` async; do not import it as a module.",
2844
+ ...COMMON_NOTES
2845
+ ]
2846
+ },
2847
+ sveltekit: {
2848
+ framework: "sveltekit",
2849
+ location: "src/app.html",
2850
+ placement: "inside <head>, above %sveltekit.head%",
2851
+ notes: [
2852
+ "src/app.html is the single HTML shell for the whole app \u2014 the snippet belongs in its <head>.",
2853
+ "Placing it above %sveltekit.head% guarantees it loads before route components.",
2854
+ ...COMMON_NOTES
2855
+ ]
2856
+ },
2857
+ vite: {
2858
+ framework: "vite",
2859
+ location: "index.html",
2860
+ placement: "just before </head> in the project root index.html",
2861
+ notes: [
2862
+ "Vite serves index.html as the entry \u2014 add the snippet to its <head> directly.",
2863
+ ...COMMON_NOTES
2864
+ ]
2865
+ },
2866
+ html: {
2867
+ framework: "html",
2868
+ location: "index.html",
2869
+ placement: "just before </head> on every page",
2870
+ notes: [
2871
+ "Static site: add the snippet to the <head> of every HTML page (or your shared header include/partial).",
2872
+ ...COMMON_NOTES
2873
+ ]
2874
+ },
2875
+ unknown: {
2876
+ framework: "unknown",
2877
+ location: "your site's root HTML / <head>",
2878
+ placement: "just before the closing </head> tag on every page",
2879
+ notes: [
2880
+ "Stack not auto-detected: place the snippet in the <head> of every page via whatever shared template/layout your site uses.",
2881
+ ...COMMON_NOTES
2882
+ ]
2883
+ }
2884
+ };
2885
+ function getStackGuide(framework) {
2886
+ return STACK_GUIDES[framework] ?? STACK_GUIDES.unknown;
2887
+ }
2888
+ function getPlatformPluginGuide(platform, accountId) {
2889
+ const product = {
2890
+ wordpress: "SealMetrics for WordPress plugin",
2891
+ woocommerce: "SealMetrics for WooCommerce plugin",
2892
+ prestashop: "SealMetrics PrestaShop module",
2893
+ magento2: "SealMetrics Magento 2 extension",
2894
+ drupal: "SealMetrics Drupal module",
2895
+ joomla: "SealMetrics Joomla plugin",
2896
+ opencart: "SealMetrics OpenCart extension"
2897
+ };
2898
+ return `Install the official ${product[platform]} (see integrations/sealmetrics-${platform}.zip), then paste your account_id \`${accountId}\` into its settings. Do not edit theme/PHP files or paste the raw <script> snippet \u2014 the plugin injects the tracker correctly.`;
2899
+ }
2900
+
2901
+ // dist/tools/setup.js
2902
+ var TOS_URL = "https://sealmetrics.com/terms";
2903
+ function str(v) {
2904
+ if (v === void 0 || v === null)
2905
+ return void 0;
2906
+ const s = String(v).trim();
2907
+ return s.length > 0 ? s : void 0;
2908
+ }
2909
+ function createSetupTools(ctx) {
2910
+ const provisionSite = {
2911
+ name: "provision_site",
2912
+ description: "Register a NEW free SealMetrics site from the chat (no terminal needed). Creates the account, returns the tracker snippet to place in your site's <head>, and emails you a claim link to set a password. After this succeeds, the read-only analytics tools are enabled in this session. Does NOT edit your code \u2014 you (or the agent, if it has the repo) place the snippet.",
2913
+ inputSchema: {
2914
+ type: "object",
2915
+ properties: {
2916
+ site_name: { type: "string", description: "Human-friendly name for the site (e.g. 'My Shop')." },
2917
+ domain: { type: "string", description: "Primary domain of the site (e.g. 'myshop.com'). Optional." },
2918
+ email: { type: "string", description: "Your email \u2014 receives the claim link to set a password." },
2919
+ name: { type: "string", description: "Your name. Optional." },
2920
+ accept_terms: {
2921
+ type: "boolean",
2922
+ description: "Must be true: confirms the user has read and accepts the SealMetrics Terms of Service (https://sealmetrics.com/terms). Show the user this link first; do NOT accept on their behalf."
2923
+ }
2924
+ },
2925
+ required: ["site_name", "email", "accept_terms"]
439
2926
  },
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"],
2927
+ annotations: { title: "Provision a SealMetrics site", readOnlyHint: false, openWorldHint: true },
2928
+ handler: async (args) => {
2929
+ if (args.accept_terms !== true && args.accept_terms !== "true") {
2930
+ throw new Error(`accept_terms must be true. Show the user the SealMetrics Terms of Service (${TOS_URL}) and ask them to confirm \u2014 do not accept on their behalf.`);
2931
+ }
2932
+ const siteName = str(args.site_name);
2933
+ const email = str(args.email);
2934
+ if (!siteName)
2935
+ throw new Error("site_name is required.");
2936
+ if (!email)
2937
+ throw new Error("email is required.");
2938
+ const input = {
2939
+ siteName,
2940
+ domain: str(args.domain),
2941
+ email,
2942
+ name: str(args.name),
2943
+ installSource: ctx.installSource
2944
+ // "mcp" — attribution (RF-3205/RF-3604)
2945
+ };
2946
+ let envelope;
2947
+ try {
2948
+ envelope = await ctx.client.post("/provision", buildProvisionBody(input), {
2949
+ provisionKey: ctx.provisionKey
2950
+ });
2951
+ } catch (e) {
2952
+ if (e instanceof SealMetricsAPIError) {
2953
+ const pe = provisionErrorForStatus(e.statusCode);
2954
+ throw new Error(`${pe.code}: ${pe.message}`);
2955
+ }
2956
+ throw e;
2957
+ }
2958
+ const result = unwrapProvisionData(envelope);
2959
+ ctx.onProvisioned(result.api_key, result.account_id);
2960
+ ctx.state.provisioned = true;
2961
+ ctx.state.accountId = result.account_id;
2962
+ return {
2963
+ account_id: result.account_id,
2964
+ snippet: result.snippet,
2965
+ dashboard_url: result.dashboard_url,
2966
+ claim_email_sent_to: email,
2967
+ free_quota: result.free_quota,
2968
+ next_steps: [
2969
+ "Place the snippet in your site's <head> on every page. If the agent has your repo it can do this; otherwise paste it yourself.",
2970
+ "Run verify_setup once the snippet is live to confirm the pixel is sending data.",
2971
+ "Read-only analytics tools are now enabled in THIS session.",
2972
+ "To keep them after restarting Claude Desktop, paste the api_key from your welcome email into the SEALMETRICS_API_KEY field of the extension settings.",
2973
+ "Check your email to claim the account (set a password) \u2014 this unlocks the web dashboard. Analytics work without it."
2974
+ ]
2975
+ };
2976
+ }
2977
+ };
2978
+ const verifySetup = {
2979
+ name: "verify_setup",
2980
+ description: "Poll until the SealMetrics pixel is confirmed installed (a real pageview has reached the backend) or it times out. Run this after placing the snippet. Reads only \u2014 sends no data.",
2981
+ inputSchema: {
2982
+ type: "object",
2983
+ properties: {
2984
+ account_id: {
2985
+ type: "string",
2986
+ description: "Site/account id to verify. Defaults to the site provisioned in this session."
487
2987
  },
2988
+ timeout_seconds: {
2989
+ type: "number",
2990
+ description: "Max seconds to wait for the first hit (default 25)."
2991
+ }
2992
+ }
488
2993
  },
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
- },
2994
+ annotations: { title: "Verify pixel install", readOnlyHint: true, openWorldHint: true },
2995
+ handler: async (args) => {
2996
+ const accountId = str(args.account_id) ?? ctx.state.accountId;
2997
+ if (!accountId)
2998
+ throw new Error("account_id is required (provision a site first).");
2999
+ const apiKey = ctx.client.getApiKey();
3000
+ if (!apiKey) {
3001
+ throw new Error("AUTH_REQUIRED: no api_key available to verify. Provision a site first, or set SEALMETRICS_API_KEY.");
3002
+ }
3003
+ const timeoutMs = ctx.pollDefaults?.timeoutMs ?? (typeof args.timeout_seconds === "number" ? Math.max(0, args.timeout_seconds) * 1e3 : 25e3);
3004
+ const intervalMs = ctx.pollDefaults?.intervalMs ?? 2e3;
3005
+ const res = await pollPixelStatus({
3006
+ fetchStatus: () => fetchPixelStatus(accountId, { baseUrl: ctx.baseUrl, apiKey }),
3007
+ timeoutMs,
3008
+ intervalMs
3009
+ });
3010
+ if (res.verified === true)
3011
+ ctx.state.pixelVerified = true;
3012
+ return {
3013
+ account_id: accountId,
3014
+ installed: res.verified === true,
3015
+ status: res.verified === true ? "verified" : "pending",
3016
+ total_hits: res.totalHits ?? 0,
3017
+ next_steps: res.verified === true ? ["Pixel confirmed. You can now query analytics with the read-only tools."] : [
3018
+ "No hits yet. Make sure the snippet is in the <head> of a live page, then load that page and re-run verify_setup."
3019
+ ]
3020
+ };
3021
+ }
3022
+ };
3023
+ const getSetupStatus = {
3024
+ name: "get_setup_status",
3025
+ description: "Report where the setup flow is: whether a site has been provisioned in this session and whether its pixel has been verified.",
3026
+ inputSchema: { type: "object", properties: {} },
3027
+ annotations: { title: "Setup status", readOnlyHint: true },
3028
+ handler: async () => ({
3029
+ provisioned: ctx.state.provisioned,
3030
+ account_id: ctx.state.accountId,
3031
+ pixel_verified: ctx.state.pixelVerified
3032
+ })
3033
+ };
3034
+ const detectFrameworkTool = {
3035
+ name: "detect_framework",
3036
+ description: "Best-effort detect the web framework/CMS of a project so the snippet can be placed correctly. Pass `path` if you have the repo; in a pure chat (no repo) returns 'unknown' plus the manual guide. Read-only \u2014 never edits files.",
3037
+ inputSchema: {
3038
+ type: "object",
3039
+ properties: {
3040
+ path: {
3041
+ type: "string",
3042
+ description: "Absolute path to the project root. Optional; omit if you don't have the repo."
3043
+ }
3044
+ }
521
3045
  },
522
- ];
523
- // Initialize server
524
- const server = new Server({
525
- name: "sealmetrics",
526
- version: "0.1.0",
527
- }, {
528
- capabilities: {
529
- tools: {},
3046
+ annotations: { title: "Detect framework", readOnlyHint: true },
3047
+ handler: async (args) => {
3048
+ const path = str(args.path) ?? ctx.cwd;
3049
+ if (!path) {
3050
+ const guide2 = getStackGuide("unknown");
3051
+ return {
3052
+ framework: "unknown",
3053
+ strategy: "manual",
3054
+ location: guide2.location,
3055
+ placement: guide2.placement,
3056
+ provision_only: true,
3057
+ note: "No project path available (e.g. Claude Desktop chat). Provide `path` if you have the repo, or place the snippet manually using loading_notes.",
3058
+ loading_notes: guide2.notes
3059
+ };
3060
+ }
3061
+ const d = detectFramework(path);
3062
+ const guide = getStackGuide(d.framework);
3063
+ return {
3064
+ framework: d.framework,
3065
+ platform: d.platform,
3066
+ strategy: d.strategy,
3067
+ location: d.recommendedLocation,
3068
+ placement: d.placementHint,
3069
+ provision_only: d.provisionOnly,
3070
+ loading_notes: guide.notes,
3071
+ ...d.platform ? { plugin_guidance: getPlatformPluginGuide(d.platform, ctx.state.accountId ?? "[YOUR_ACCOUNT_ID]") } : {}
3072
+ };
3073
+ }
3074
+ };
3075
+ const getInstrumentationGuideTool = {
3076
+ name: "get_instrumentation_guide",
3077
+ description: "Return the canonical SealMetrics event-instrumentation guide (closed conv/micro taxonomy + privacy rules) with your account_id substituted. Use it before writing sealmetrics.conv()/micro() calls. The MCP does NOT write these calls \u2014 the agent does, guided by this.",
3078
+ inputSchema: {
3079
+ type: "object",
3080
+ properties: {
3081
+ account_id: {
3082
+ type: "string",
3083
+ description: "Account id to substitute into the guide. Defaults to the provisioned site."
3084
+ }
3085
+ }
530
3086
  },
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 }] };
3087
+ annotations: { title: "Instrumentation guide", readOnlyHint: true },
3088
+ handler: async (args) => {
3089
+ const accountId = str(args.account_id) ?? ctx.state.accountId ?? "[YOUR_ACCOUNT_ID]";
3090
+ return { account_id: accountId, guide: getInstrumentationGuide(accountId) };
3091
+ }
3092
+ };
3093
+ const verifyEventInstrumented = {
3094
+ name: "verify_event_instrumented",
3095
+ description: "Close the instrumentation loop (Fase 3 Bloque 4): after you add a sealmetrics.conv()/micro() call AND trigger it (a test visit/action), confirm the event reached the backend with the expected type. ALSO validates the event name against the closed taxonomy and rejects PII in properties before declaring success. Reads only.",
3096
+ inputSchema: {
3097
+ type: "object",
3098
+ properties: {
3099
+ account_id: { type: "string", description: "Site/account id. Defaults to the provisioned site." },
3100
+ kind: { type: "string", enum: ["conv", "micro"], description: "Event kind: 'conv' or 'micro'." },
3101
+ name: { type: "string", description: "Event name (must be in the closed taxonomy, e.g. 'purchase')." },
3102
+ timeout_seconds: { type: "number", description: "Max seconds to wait for the event (default 25)." }
3103
+ },
3104
+ required: ["kind", "name"]
3105
+ },
3106
+ annotations: { title: "Verify instrumented event", readOnlyHint: true, openWorldHint: true },
3107
+ handler: async (args) => {
3108
+ const accountId = str(args.account_id) ?? ctx.state.accountId;
3109
+ if (!accountId)
3110
+ throw new Error("account_id is required (provision a site first).");
3111
+ const kind = str(args.kind);
3112
+ const name = str(args.name);
3113
+ if (kind !== "conv" && kind !== "micro")
3114
+ throw new Error("kind must be 'conv' or 'micro'.");
3115
+ if (!name)
3116
+ throw new Error("name is required.");
3117
+ const apiKey = ctx.client.getApiKey();
3118
+ if (!apiKey)
3119
+ throw new Error("AUTH_REQUIRED: no api_key available. Provision a site first or set SEALMETRICS_API_KEY.");
3120
+ const check = checkInstrumentation(kind, name);
3121
+ if (!check.taxonomy.valid) {
3122
+ return {
3123
+ status: "rejected",
3124
+ reason: "out_of_taxonomy",
3125
+ name,
3126
+ suggestion: check.taxonomy.suggestion,
3127
+ message: `'${name}' is not in the closed ${kind} taxonomy.${check.taxonomy.suggestion ? ` Did you mean '${check.taxonomy.suggestion}'?` : ""} Fix the event name before verifying.`
3128
+ };
3129
+ }
3130
+ const path = kind === "conv" ? "/stats/conversions/raw" : "/stats/microconversions/raw";
3131
+ const typeParam = kind === "conv" ? "conversion_type" : "microconversion_type";
3132
+ const timeoutMs = ctx.pollDefaults?.timeoutMs ?? (typeof args.timeout_seconds === "number" ? Math.max(0, args.timeout_seconds) * 1e3 : 25e3);
3133
+ const intervalMs = ctx.pollDefaults?.intervalMs ?? 3e3;
3134
+ const startedAt = ctx.now ? ctx.now() : Date.now();
3135
+ const deadline = startedAt + timeoutMs;
3136
+ let found;
3137
+ let piiFlagged = [];
3138
+ for (; ; ) {
3139
+ try {
3140
+ const raw = await ctx.client.requestDirect(path, {
3141
+ site_id: accountId,
3142
+ period: "today",
3143
+ [typeParam]: name,
3144
+ include_properties: "true",
3145
+ page_size: "20"
3146
+ });
3147
+ const rows = Array.isArray(raw?.data) ? raw.data : [];
3148
+ for (const row of rows) {
3149
+ const ts = Date.parse(String(row.timestamp_utc ?? ""));
3150
+ if (Number.isFinite(ts) && ts >= startedAt - 5e3) {
3151
+ found = row;
3152
+ const findings = detectPropertiesPII(row.properties);
3153
+ piiFlagged = findings;
3154
+ break;
783
3155
  }
784
- default:
785
- return {
786
- content: [{ type: "text", text: `Unknown tool: ${name}` }],
787
- };
3156
+ }
3157
+ } catch {
788
3158
  }
789
- }
790
- catch (error) {
791
- const message = error instanceof Error ? error.message : String(error);
3159
+ if (found)
3160
+ break;
3161
+ const nowMs = ctx.now ? ctx.now() : Date.now();
3162
+ if (nowMs + intervalMs >= deadline)
3163
+ break;
3164
+ await sleep2(intervalMs, ctx.sleep);
3165
+ }
3166
+ if (!found) {
3167
+ return {
3168
+ status: "pending",
3169
+ account_id: accountId,
3170
+ kind,
3171
+ name,
3172
+ message: `No '${name}' ${kind} event seen yet. Trigger the event (a test visit/action), then re-run. Raw endpoints lag ~2-5s.`
3173
+ };
3174
+ }
3175
+ if (piiFlagged.length > 0) {
792
3176
  return {
793
- content: [{ type: "text", text: `Error: ${message}` }],
3177
+ status: "warning_pii",
3178
+ account_id: accountId,
3179
+ kind,
3180
+ name,
3181
+ pii_properties: piiFlagged,
3182
+ message: `Event '${name}' arrived, but its properties look like PII (${piiFlagged.map((f) => f.key).join(", ")}). Remove personal data / order/user IDs \u2014 these must NEVER be tracked.`
794
3183
  };
3184
+ }
3185
+ return {
3186
+ status: "verified",
3187
+ account_id: accountId,
3188
+ kind,
3189
+ name,
3190
+ message: `Event '${name}' confirmed in SealMetrics with no PII. Instrumentation verified.`
3191
+ };
795
3192
  }
3193
+ };
3194
+ return [
3195
+ provisionSite,
3196
+ verifySetup,
3197
+ getSetupStatus,
3198
+ detectFrameworkTool,
3199
+ getInstrumentationGuideTool,
3200
+ verifyEventInstrumented
3201
+ ];
3202
+ }
3203
+ function sleep2(ms, custom) {
3204
+ if (custom)
3205
+ return custom(ms);
3206
+ return new Promise((r) => setTimeout(r, ms));
3207
+ }
3208
+ function detectPropertiesPII(properties) {
3209
+ if (!properties || typeof properties !== "object")
3210
+ return [];
3211
+ return detectPII(properties).map((f) => ({ reason: f.reason, key: f.key }));
3212
+ }
3213
+
3214
+ // dist/embedded.js
3215
+ var EMBEDDED_PROVISION_KEY = "pk_mcp_69bb5a1ec96f2cc4d5dd77be";
3216
+ var FALLBACK_PROVISION_KEY = "pk_provision_fallback";
3217
+ var INSTALL_SOURCE = "mcp";
3218
+ function isNonProdTarget(baseUrl) {
3219
+ return /localhost|127\.0\.0\.1|pre\.sealmetrics\.com/.test(baseUrl);
3220
+ }
3221
+ function resolveProvisionKey(baseUrl) {
3222
+ if (process.env.SEALMETRICS_PROVISION_KEY)
3223
+ return process.env.SEALMETRICS_PROVISION_KEY;
3224
+ if (isNonProdTarget(baseUrl))
3225
+ return FALLBACK_PROVISION_KEY;
3226
+ return EMBEDDED_PROVISION_KEY;
3227
+ }
3228
+
3229
+ // dist/resources/tracking-guide.js
3230
+ var TRACKING_GUIDE_URI = "sealmetrics://tracking-guide";
3231
+ var TRACKING_GUIDE_NAME = "SealMetrics Tracking Guide";
3232
+ var TRACKING_GUIDE_DESCRIPTION = "Operational playbook for implementing SealMetrics tracking on any website: pixel installation, conversions, microconversions, content grouping, and framework-specific patterns.";
3233
+ var TRACKING_GUIDE_CONTENT = `# SealMetrics Tracking \u2014 Implementation Playbook
3234
+
3235
+ You are implementing SealMetrics analytics on a website. This guide tells you exactly what to do, step by step.
3236
+
3237
+ ---
3238
+
3239
+ ## Step 1: Get the pixel
3240
+
3241
+ Call the \`get_tracking_code\` tool with the user's \`site_id\`. It returns:
3242
+ - \`script_tag\`: the exact \`<script>\` tag to insert (with the real site ID baked in)
3243
+ - \`tracker_url\`: the URL of the tracker JS file
3244
+
3245
+ If you don't know the site_id, call \`list_sites\` first to find it.
3246
+
3247
+ ---
3248
+
3249
+ ## Step 2: Find where to insert the script
3250
+
3251
+ The script tag goes in the \`<head>\` of every page, **once**. Where that is depends on the framework:
3252
+
3253
+ | Framework | Where to put it |
3254
+ |-----------|----------------|
3255
+ | **Plain HTML** | Inside \`<head>\` in every \`.html\` file, or in a shared template/layout |
3256
+ | **Next.js (App Router)** | \`app/layout.tsx\` \u2014 use \`import Script from 'next/script'\` with \`strategy="afterInteractive"\` |
3257
+ | **Next.js (Pages Router)** | \`pages/_app.tsx\` \u2014 use \`<Script>\` component |
3258
+ | **React (CRA/Vite)** | \`index.html\` inside \`<head>\` |
3259
+ | **Vue / Nuxt** | \`nuxt.config.ts\` head section, or \`app.vue\` |
3260
+ | **Angular** | \`src/index.html\` inside \`<head>\` |
3261
+ | **Astro** | Shared layout component \`<head>\` |
3262
+ | **WordPress** | \`header.php\` or via a "header scripts" plugin/setting |
3263
+ | **Shopify** | \`theme.liquid\` inside \`<head>\` |
3264
+
3265
+ **How to find it**: Search for \`</head>\`, or look for the root layout/template file. Every framework has one place that wraps all pages \u2014 that's where the script goes.
3266
+
3267
+ ### Plain HTML
3268
+ \`\`\`html
3269
+ <head>
3270
+ <!-- other tags... -->
3271
+ <script src="https://t.sealmetrics.com/t.js?id=SITE_ID" defer></script>
3272
+ </head>
3273
+ \`\`\`
3274
+
3275
+ ### Next.js (App Router)
3276
+ \`\`\`tsx
3277
+ // app/layout.tsx
3278
+ import Script from 'next/script';
3279
+
3280
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
3281
+ return (
3282
+ <html lang="en">
3283
+ <head />
3284
+ <body>
3285
+ {children}
3286
+ <Script src="https://t.sealmetrics.com/t.js?id=SITE_ID" strategy="afterInteractive" />
3287
+ </body>
3288
+ </html>
3289
+ );
3290
+ }
3291
+ \`\`\`
3292
+
3293
+ ### Vue / Nuxt
3294
+ \`\`\`ts
3295
+ // nuxt.config.ts
3296
+ export default defineNuxtConfig({
3297
+ app: {
3298
+ head: {
3299
+ script: [{ src: 'https://t.sealmetrics.com/t.js?id=SITE_ID', defer: true }]
3300
+ }
3301
+ }
796
3302
  });
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);
3303
+ \`\`\`
3304
+
3305
+ **Once the script is in place, pageviews are tracked automatically** \u2014 on load, on SPA navigation (pushState, replaceState, popstate), on back/forward. You do NOT need to add manual pageview calls.
3306
+
3307
+ ---
3308
+
3309
+ ## Step 3: Identify what to track
3310
+
3311
+ Analyze the website code and identify trackeable user actions. Classify each one as a **conversion** or a **microconversion**.
3312
+
3313
+ ### What is a conversion?
3314
+
3315
+ A conversion is a **business goal** \u2014 the main thing the site owner wants users to do. It typically has monetary value or represents a completed transaction.
3316
+
3317
+ | What you see in the code | Conversion type | Has value? |
3318
+ |--------------------------|-----------------|------------|
3319
+ | Purchase/checkout completion, order confirmation page | \`purchase\` | Yes \u2014 the order total |
3320
+ | Subscription payment, plan upgrade | \`purchase\` | Yes \u2014 the subscription price |
3321
+ | Contact form, demo request, quote request | \`lead\` | No (use \`0\`) |
3322
+ | Account registration, signup form | \`signup\` | No (use \`0\`) |
3323
+ | Booking confirmation, appointment scheduled | \`booking\` | Yes if there's a price, otherwise \`0\` |
3324
+
3325
+ **Rule of thumb**: If the business would pay money to make this action happen, it's a conversion.
3326
+
3327
+ ### What is a microconversion?
3328
+
3329
+ A microconversion is a **step toward a conversion** or an **engagement signal**. It tells you how users interact with the site before converting.
3330
+
3331
+ | What you see in the code | Microconversion type |
3332
+ |--------------------------|---------------------|
3333
+ | "Add to cart" button | \`add_to_cart\` |
3334
+ | "Add to wishlist" / "Save for later" | \`add_to_wishlist\` |
3335
+ | Checkout step buttons (shipping, payment...) | \`begin_checkout\`, \`checkout_shipping\`, \`checkout_payment\` |
3336
+ | Newsletter/email signup form | \`newsletter_signup\` |
3337
+ | PDF/resource download link | \`download\` |
3338
+ | Video play button | \`video_play\` |
3339
+ | Video ends | \`video_complete\` |
3340
+ | Pricing page visited or pricing toggle clicked | \`pricing_view\` |
3341
+ | Share/social buttons | \`share\` |
3342
+ | Search form used | \`search\` |
3343
+ | Filter/sort applied | \`filter_applied\` |
3344
+ | Tab/accordion clicked to reveal content | \`content_expand\` |
3345
+ | Chat widget opened | \`chat_open\` |
3346
+ | Scroll milestones (25%, 50%, 75%, 100%) | \`scroll_25\`, \`scroll_50\`, \`scroll_75\`, \`scroll_100\` |
3347
+
3348
+ **Rule of thumb**: If it shows intent or engagement but isn't the final goal, it's a microconversion.
3349
+
3350
+ ---
3351
+
3352
+ ## Step 4: Implement conversions
3353
+
3354
+ ### Syntax
3355
+ \`\`\`js
3356
+ sealmetrics.conv(type, amount, properties?)
3357
+ \`\`\`
3358
+
3359
+ | Parameter | Type | Required | Description |
3360
+ |-----------|------|----------|-------------|
3361
+ | \`type\` | string | Yes | snake_case name for this conversion |
3362
+ | \`amount\` | number | Yes | Monetary value. Use \`0\` if no value (leads, signups) |
3363
+ | \`properties\` | object | No | Extra metadata (see "Using properties" below) |
3364
+
3365
+ ### Where to put it
3366
+
3367
+ The conversion call goes **at the moment the action succeeds**, not when the user clicks. For example:
3368
+
3369
+ - **Form submission**: In the \`onSubmit\` handler, AFTER validation passes (or on the thank-you page)
3370
+ - **Purchase**: On the order confirmation/thank-you page, or in the success callback of the payment API
3371
+ - **Signup**: After the registration API call succeeds
3372
+
3373
+ ### Examples by pattern
3374
+
3375
+ **Form submit (vanilla JS):**
3376
+ \`\`\`js
3377
+ document.querySelector('#contact-form').addEventListener('submit', function(e) {
3378
+ // The form is about to submit \u2014 track the lead
3379
+ sealmetrics.conv('lead', 0, { form_name: 'contact', page: location.pathname });
3380
+ });
3381
+ \`\`\`
3382
+
3383
+ **Form submit (React):**
3384
+ \`\`\`tsx
3385
+ const handleSubmit = async (data: FormData) => {
3386
+ await api.submitContactForm(data);
3387
+ window.sealmetrics?.conv('lead', 0, { form_name: 'contact' });
3388
+ };
3389
+ \`\`\`
3390
+
3391
+ **Purchase (thank-you page):**
3392
+ \`\`\`html
3393
+ <!-- This script runs on /order-confirmation or /thank-you -->
3394
+ <script>
3395
+ // Pull values from the page or from server-rendered data
3396
+ sealmetrics.conv('purchase', 149.99, {
3397
+ currency: 'EUR',
3398
+ payment_method: 'credit_card'
3399
+ });
3400
+ </script>
3401
+ \`\`\`
3402
+
3403
+ **Purchase (React, after payment API):**
3404
+ \`\`\`tsx
3405
+ const handlePayment = async () => {
3406
+ const result = await processPayment(cart);
3407
+ if (result.success) {
3408
+ window.sealmetrics?.conv('purchase', cart.total, {
3409
+ currency: cart.currency,
3410
+ payment_method: result.method
3411
+ });
3412
+ router.push('/thank-you');
3413
+ }
3414
+ };
3415
+ \`\`\`
3416
+
3417
+ **Signup (after API success):**
3418
+ \`\`\`tsx
3419
+ const handleRegister = async (formData: RegisterForm) => {
3420
+ const user = await api.register(formData);
3421
+ window.sealmetrics?.conv('signup', 0, { plan: formData.plan });
3422
+ router.push('/welcome');
3423
+ };
3424
+ \`\`\`
3425
+
3426
+ ---
3427
+
3428
+ ## Step 5: Implement microconversions
3429
+
3430
+ ### Syntax
3431
+ \`\`\`js
3432
+ sealmetrics.micro(type, properties?)
3433
+ \`\`\`
3434
+
3435
+ | Parameter | Type | Required | Description |
3436
+ |-----------|------|----------|-------------|
3437
+ | \`type\` | string | Yes | snake_case name for this event |
3438
+ | \`properties\` | object | No | Extra metadata (see "Using properties" below) |
3439
+
3440
+ ### Where to put it
3441
+
3442
+ Microconversions go **on the user action** \u2014 usually in click handlers, submit handlers, or event listeners.
3443
+
3444
+ ### Examples by pattern
3445
+
3446
+ **Add to cart (vanilla):**
3447
+ \`\`\`js
3448
+ document.querySelectorAll('.add-to-cart').forEach(function(btn) {
3449
+ btn.addEventListener('click', function() {
3450
+ sealmetrics.micro('add_to_cart', {
3451
+ product_id: this.dataset.productId,
3452
+ product_name: this.dataset.productName,
3453
+ price: parseFloat(this.dataset.price)
3454
+ });
3455
+ });
3456
+ });
3457
+ \`\`\`
3458
+
3459
+ **Add to cart (React):**
3460
+ \`\`\`tsx
3461
+ function AddToCartButton({ product }: { product: Product }) {
3462
+ const handleClick = () => {
3463
+ addToCart(product);
3464
+ window.sealmetrics?.micro('add_to_cart', {
3465
+ product_id: product.id,
3466
+ product_name: product.name,
3467
+ price: product.price
3468
+ });
3469
+ };
3470
+ return <button onClick={handleClick}>Add to Cart</button>;
3471
+ }
3472
+ \`\`\`
3473
+
3474
+ **Newsletter signup:**
3475
+ \`\`\`js
3476
+ document.querySelector('#newsletter-form').addEventListener('submit', function() {
3477
+ sealmetrics.micro('newsletter_signup', {
3478
+ position: this.closest('footer') ? 'footer' : 'inline'
3479
+ });
3480
+ });
3481
+ \`\`\`
3482
+
3483
+ **Video engagement:**
3484
+ \`\`\`js
3485
+ var video = document.querySelector('video');
3486
+ video.addEventListener('play', function() {
3487
+ sealmetrics.micro('video_play', { video_id: this.dataset.videoId });
3488
+ });
3489
+ video.addEventListener('ended', function() {
3490
+ sealmetrics.micro('video_complete', { video_id: this.dataset.videoId });
3491
+ });
3492
+ \`\`\`
3493
+
3494
+ **Scroll depth tracking:**
3495
+ \`\`\`js
3496
+ var scrollTracked = {};
3497
+ window.addEventListener('scroll', function() {
3498
+ var pct = Math.round(window.scrollY / (document.body.scrollHeight - window.innerHeight) * 100);
3499
+ [25, 50, 75, 100].forEach(function(milestone) {
3500
+ if (pct >= milestone && !scrollTracked[milestone]) {
3501
+ scrollTracked[milestone] = true;
3502
+ sealmetrics.micro('scroll_' + milestone);
802
3503
  }
803
- const transport = new StdioServerTransport();
804
- await server.connect(transport);
3504
+ });
3505
+ });
3506
+ \`\`\`
3507
+
3508
+ **Checkout funnel steps (React):**
3509
+ \`\`\`tsx
3510
+ // When user moves from step to step
3511
+ const goToStep = (step: number) => {
3512
+ setCurrentStep(step);
3513
+ const stepNames: Record<number, string> = {
3514
+ 1: 'begin_checkout',
3515
+ 2: 'checkout_shipping',
3516
+ 3: 'checkout_payment',
3517
+ };
3518
+ if (stepNames[step]) {
3519
+ window.sealmetrics?.micro(stepNames[step], { items_count: cart.items.length });
3520
+ }
3521
+ };
3522
+ \`\`\`
3523
+
3524
+ ---
3525
+
3526
+ ## Step 6: Set up content grouping
3527
+
3528
+ Content grouping lets the site owner analyze metrics by section: "how does the blog perform vs product pages?"
3529
+
3530
+ ### When to use it
3531
+
3532
+ Use content grouping when the site has **distinct sections** that serve different purposes. If the site is a single-purpose landing page, skip it.
3533
+
3534
+ ### How to decide groups
3535
+
3536
+ Look at the URL structure and page purpose:
3537
+
3538
+ | URL pattern | Group |
3539
+ |-------------|-------|
3540
+ | \`/blog/*\`, \`/posts/*\`, \`/articles/*\` | \`blog\` |
3541
+ | \`/products/*\`, \`/shop/*\`, \`/item/*\` | \`product\` |
3542
+ | \`/category/*\`, \`/collections/*\` | \`category\` |
3543
+ | \`/cart\`, \`/checkout/*\`, \`/order/*\` | \`checkout\` |
3544
+ | \`/docs/*\`, \`/help/*\`, \`/faq\` | \`docs\` |
3545
+ | \`/dashboard/*\`, \`/app/*\`, \`/account/*\` | \`app\` |
3546
+ | \`/pricing\`, \`/plans\` | \`pricing\` |
3547
+ | \`/\`, \`/about\`, \`/features\`, \`/contact\` | \`landing\` |
3548
+
3549
+ ### How to implement it
3550
+
3551
+ **Option A \u2014 Static (via URL param in the script tag):**
3552
+
3553
+ Best when you can set a different script tag per section (e.g., different templates, layouts).
3554
+
3555
+ \`\`\`html
3556
+ <!-- Blog layout -->
3557
+ <script src="https://t.sealmetrics.com/t.js?id=SITE_ID&group=blog" defer></script>
3558
+
3559
+ <!-- Product layout -->
3560
+ <script src="https://t.sealmetrics.com/t.js?id=SITE_ID&group=product" defer></script>
3561
+ \`\`\`
3562
+
3563
+ **Option B \u2014 Dynamic (via JS, based on URL):**
3564
+
3565
+ Best for SPAs or when you can't change the script tag per section.
3566
+
3567
+ \`\`\`js
3568
+ // Determine group from the current path
3569
+ function getContentGroup() {
3570
+ var path = location.pathname;
3571
+ if (path.startsWith('/blog') || path.startsWith('/posts')) return 'blog';
3572
+ if (path.startsWith('/products') || path.startsWith('/shop')) return 'product';
3573
+ if (path.startsWith('/cart') || path.startsWith('/checkout')) return 'checkout';
3574
+ if (path.startsWith('/docs') || path.startsWith('/help')) return 'docs';
3575
+ return 'landing';
3576
+ }
3577
+
3578
+ sealmetrics({ group: getContentGroup() });
3579
+ \`\`\`
3580
+
3581
+ **Option C \u2014 Next.js (per layout segment):**
3582
+ \`\`\`tsx
3583
+ // app/blog/layout.tsx
3584
+ import Script from 'next/script';
3585
+ export default function BlogLayout({ children }: { children: React.ReactNode }) {
3586
+ return (
3587
+ <>
3588
+ <Script src="https://t.sealmetrics.com/t.js?id=SITE_ID&group=blog" strategy="afterInteractive" />
3589
+ {children}
3590
+ </>
3591
+ );
3592
+ }
3593
+ \`\`\`
3594
+ Note: if you use per-layout scripts with groups, remove the global script from the root layout to avoid double-tracking.
3595
+
3596
+ ---
3597
+
3598
+ ## Using properties effectively
3599
+
3600
+ Properties are key-value metadata attached to conversions and microconversions. They power drill-down analysis in the dashboard.
3601
+
3602
+ ### What to include
3603
+
3604
+ | Property | When to use | Example |
3605
+ |----------|------------|---------|
3606
+ | \`currency\` | Any monetary conversion | \`'EUR'\`, \`'USD'\` |
3607
+ | \`payment_method\` | Purchase conversions | \`'credit_card'\`, \`'paypal'\`, \`'stripe'\` |
3608
+ | \`plan\` | Signup/subscription conversions | \`'free'\`, \`'pro'\`, \`'enterprise'\` |
3609
+ | \`product_id\` | Product-related events | \`'SKU-123'\` |
3610
+ | \`product_name\` | Product-related events | \`'Wireless Headphones'\` |
3611
+ | \`price\` | Add-to-cart, wishlist | \`49.99\` |
3612
+ | \`category\` | Product events | \`'electronics'\`, \`'shoes'\` |
3613
+ | \`form_name\` | Lead/form conversions | \`'contact'\`, \`'demo_request'\`, \`'quote'\` |
3614
+ | \`position\` | UI element interactions | \`'header'\`, \`'footer'\`, \`'popup'\`, \`'sidebar'\` |
3615
+ | \`video_id\` | Video events | \`'intro-video'\`, \`'product-demo'\` |
3616
+ | \`search_term\` | Search events | The user's search query |
3617
+
3618
+ ### What NOT to include
3619
+
3620
+ - **Order IDs, user IDs, transaction IDs** \u2014 these are high-cardinality and don't help analysis. Never put them in the type name either.
3621
+ - **Email addresses, names, or PII** \u2014 SealMetrics is privacy-first.
3622
+ - **Timestamps** \u2014 the system already records when events happen.
3623
+ - **Page URLs** \u2014 already tracked automatically.
3624
+
3625
+ ### Property values
3626
+
3627
+ - Keep values **low-cardinality** when possible: \`'credit_card'\` not \`'visa_ending_4242'\`
3628
+ - Use **consistent values**: always \`'credit_card'\`, never sometimes \`'cc'\` and sometimes \`'Credit Card'\`
3629
+ - **Numbers** should be numbers, not strings: \`price: 49.99\`, not \`price: '49.99'\`
3630
+
3631
+ ---
3632
+
3633
+ ## Naming conventions
3634
+
3635
+ - **snake_case** always: \`add_to_cart\`, not \`addToCart\` or \`Add To Cart\`
3636
+ - **Descriptive**: \`begin_checkout\` not \`step2\`, \`newsletter_signup\` not \`nl\`
3637
+ - **Stable**: once you name a type, don't rename it \u2014 it breaks historical data
3638
+ - **No IDs in the type name**: \`sealmetrics.conv('purchase', 99)\`, not \`sealmetrics.conv('purchase_order_12345', 99)\`
3639
+
3640
+ ---
3641
+
3642
+ ## TypeScript type declarations
3643
+
3644
+ If the project uses TypeScript, add this declaration so \`window.sealmetrics\` doesn't show type errors:
3645
+
3646
+ \`\`\`ts
3647
+ // types/sealmetrics.d.ts (or at the top of a component file)
3648
+ declare global {
3649
+ interface Window {
3650
+ sealmetrics?: {
3651
+ (options?: { group?: string }): void;
3652
+ conv: (type: string, amount: number, properties?: Record<string, unknown>) => void;
3653
+ micro: (type: string, properties?: Record<string, unknown>) => void;
3654
+ };
3655
+ }
3656
+ }
3657
+ \`\`\`
3658
+
3659
+ Then call via \`window.sealmetrics?.conv(...)\` instead of \`sealmetrics.conv(...)\`.
3660
+
3661
+ ---
3662
+
3663
+ ## Complete implementation checklist
3664
+
3665
+ Use this to verify you haven't missed anything:
3666
+
3667
+ 1. **Pixel installed**: Script tag is in the \`<head>\` (or root layout) of every page \u2014 ONE time only
3668
+ 2. **Pageviews working**: Automatic, no code needed. Verify with \`?debug=1\`
3669
+ 3. **Conversions identified**: Every business goal (purchase, lead, signup) has a \`sealmetrics.conv()\` call
3670
+ 4. **Conversion placement**: Each conversion fires at the right moment (after success, not on click)
3671
+ 5. **Conversion values**: Purchases have the real \`amount\`, leads/signups use \`0\`
3672
+ 6. **Microconversions identified**: Funnel steps and engagement signals have \`sealmetrics.micro()\` calls
3673
+ 7. **Properties added**: Events carry useful metadata (currency, product_id, form_name, etc.)
3674
+ 8. **Content grouping**: If the site has distinct sections, groups are assigned
3675
+ 9. **Naming**: All types are snake_case, descriptive, and stable
3676
+ 10. **No PII**: No emails, user IDs, or personal data in properties
3677
+
3678
+ ---
3679
+
3680
+ ## Debugging
3681
+
3682
+ Add \`?debug=1\` to any page URL to see all tracking events in the browser console:
3683
+
3684
+ \`\`\`
3685
+ https://yoursite.com/products?debug=1
3686
+ \`\`\`
3687
+
3688
+ Shows: session ID, account ID, event type, payload, and any errors.
3689
+
3690
+ ---
3691
+
3692
+ ## Technical notes
3693
+
3694
+ - **No cookies, no localStorage** \u2014 GDPR-friendly, no consent banner needed
3695
+ - **SPA support is automatic** \u2014 History API (pushState/replaceState/popstate) is intercepted
3696
+ - **Uses sendBeacon** \u2014 non-blocking, guaranteed delivery even on tab close
3697
+ - **Three equivalent globals**: \`sealmetrics\`, \`sm\`, \`_sm\` \u2014 use \`sealmetrics\` in new code
3698
+ - **Script loads async** with \`defer\` \u2014 never blocks page rendering
3699
+ `;
3700
+
3701
+ // dist/server.js
3702
+ var MAX_RESPONSE_LENGTH = 1e5;
3703
+ function jsonSchemaToZod(prop) {
3704
+ const type = prop.type;
3705
+ const enumValues = prop.enum;
3706
+ const description = prop.description;
3707
+ let schema;
3708
+ if (enumValues && enumValues.length > 0) {
3709
+ schema = z.enum(enumValues);
3710
+ } else if (type === "number" || type === "integer") {
3711
+ schema = z.number();
3712
+ } else if (type === "boolean") {
3713
+ schema = z.boolean();
3714
+ } else {
3715
+ schema = z.string();
3716
+ }
3717
+ if (description)
3718
+ schema = schema.describe(description);
3719
+ return schema;
3720
+ }
3721
+ function buildShape(properties) {
3722
+ const shape = {};
3723
+ for (const [key, prop] of Object.entries(properties)) {
3724
+ shape[key] = jsonSchemaToZod(prop).optional();
3725
+ }
3726
+ return shape;
3727
+ }
3728
+ function safeStringify(value) {
3729
+ try {
3730
+ const json = JSON.stringify(value, null, 2);
3731
+ if (json.length > MAX_RESPONSE_LENGTH) {
3732
+ return json.slice(0, MAX_RESPONSE_LENGTH) + "\n... (truncated)";
3733
+ }
3734
+ return json;
3735
+ } catch {
3736
+ return '{"error": "Response could not be serialized"}';
3737
+ }
3738
+ }
3739
+ function toToolResult(result) {
3740
+ return { content: [{ type: "text", text: safeStringify(result) }] };
3741
+ }
3742
+ function toErrorResult(error) {
3743
+ const message = error instanceof Error ? error.message : "Unknown error occurred";
3744
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
3745
+ }
3746
+ function buildServer(opts) {
3747
+ const client = new SealMetricsClient(opts.apiKey, opts.baseUrl);
3748
+ const server2 = new McpServer({ name: "sealmetrics", version: opts.version });
3749
+ const readOnlyHandles = ALL_TOOLS.map((tool) => {
3750
+ const handle = server2.registerTool(tool.name, {
3751
+ description: tool.description,
3752
+ inputSchema: buildShape(tool.inputSchema.properties),
3753
+ annotations: { title: tool.name, readOnlyHint: true }
3754
+ }, async (args) => {
3755
+ try {
3756
+ return toToolResult(await tool.handler(client, args));
3757
+ } catch (error) {
3758
+ return toErrorResult(error);
3759
+ }
3760
+ });
3761
+ if (!client.hasApiKey())
3762
+ handle.disable();
3763
+ return handle;
3764
+ });
3765
+ const enableReadOnlyTools = () => {
3766
+ for (const handle of readOnlyHandles) {
3767
+ if (!handle.enabled)
3768
+ handle.enable();
3769
+ }
3770
+ };
3771
+ const setupState = { provisioned: false, pixelVerified: false };
3772
+ const setupContext = {
3773
+ client,
3774
+ baseUrl: opts.baseUrl,
3775
+ provisionKey: opts.provisionKey,
3776
+ installSource: opts.installSource ?? INSTALL_SOURCE,
3777
+ cwd: opts.cwd,
3778
+ state: setupState,
3779
+ onProvisioned: (apiKey, _accountId) => {
3780
+ client.setApiKey(apiKey);
3781
+ enableReadOnlyTools();
3782
+ },
3783
+ pollDefaults: opts.pollDefaults,
3784
+ now: opts.now,
3785
+ sleep: opts.sleep
3786
+ };
3787
+ const setupTools = createSetupTools(setupContext);
3788
+ for (const tool of setupTools) {
3789
+ server2.registerTool(tool.name, {
3790
+ description: tool.description,
3791
+ inputSchema: buildShape(tool.inputSchema.properties),
3792
+ annotations: tool.annotations
3793
+ }, async (args) => {
3794
+ try {
3795
+ return toToolResult(await tool.handler(args));
3796
+ } catch (error) {
3797
+ return toErrorResult(error);
3798
+ }
3799
+ });
3800
+ }
3801
+ server2.resource(TRACKING_GUIDE_NAME, TRACKING_GUIDE_URI, { description: TRACKING_GUIDE_DESCRIPTION, mimeType: "text/markdown" }, async () => ({
3802
+ contents: [{ uri: TRACKING_GUIDE_URI, mimeType: "text/markdown", text: TRACKING_GUIDE_CONTENT }]
3803
+ }));
3804
+ return { server: server2, client, readOnlyHandles, setupTools, setupContext, enableReadOnlyTools };
3805
+ }
3806
+
3807
+ // dist/index.js
3808
+ import { createRequire } from "module";
3809
+ var PKG_VERSION = "0.0.0";
3810
+ try {
3811
+ const req = createRequire(import.meta.url);
3812
+ PKG_VERSION = req("../package.json").version ?? PKG_VERSION;
3813
+ } catch {
3814
+ }
3815
+ var API_KEY = process.env.SEALMETRICS_API_KEY;
3816
+ var BASE_URL = process.env.SEALMETRICS_BASE_URL ?? "https://my.sealmetrics.com/api/v1";
3817
+ var PROVISION_KEY = resolveProvisionKey(BASE_URL);
3818
+ if (PROVISION_KEY === "pk_mcp_PLACEHOLDER_REPLACE_BEFORE_PUBLISH") {
3819
+ console.error("[sealmetrics-mcp] WARNING: using the placeholder mcp provision key \u2014 provision_site will fail until the channel='mcp' key is configured.");
3820
+ }
3821
+ var { server } = buildServer({
3822
+ apiKey: API_KEY,
3823
+ baseUrl: BASE_URL,
3824
+ provisionKey: PROVISION_KEY,
3825
+ version: PKG_VERSION,
3826
+ // Only pass a cwd when an explicit project dir is configured. In Claude Desktop
3827
+ // chat there is no project tree → leave undefined so detect_framework returns
3828
+ // 'unknown' + the manual guide instead of scanning the launch directory.
3829
+ cwd: process.env.SEALMETRICS_PROJECT_DIR || void 0
3830
+ });
3831
+ async function main() {
3832
+ const transport = new StdioServerTransport();
3833
+ await server.connect(transport);
805
3834
  }
806
3835
  main().catch((error) => {
807
- console.error("Fatal error:", error);
808
- process.exit(1);
3836
+ console.error("Fatal error:", error);
3837
+ process.exit(1);
809
3838
  });