growtics-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Growtics
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # growtics-mcp
2
+
3
+ > Talk to your Growtics analytics with any AI assistant.
4
+
5
+ **`growtics-mcp`** is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that connects your Growtics analytics data to AI assistants like Claude Desktop, Cursor, and Windsurf.
6
+
7
+ Ask natural-language questions powered by your real data:
8
+
9
+ - *"Why did traffic drop this week?"*
10
+ - *"What should I improve next for SEO?"*
11
+ - *"Which pages are losing conversions?"*
12
+ - *"Analyze my latest release and its impact."*
13
+
14
+ ---
15
+
16
+ ## Quick Start
17
+
18
+ **No installation needed.** Add this to your AI client's MCP config and you're done:
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "growtics": {
24
+ "command": "npx",
25
+ "args": ["-y", "growtics-mcp"],
26
+ "env": {
27
+ "GROWTICS_API_KEY": "gt_live_YOUR_KEY_HERE"
28
+ }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ **Get your API key:** Growtics Dashboard → Settings → API Keys
35
+ Required scopes: `analytics:read`, `sites:read`, `realtime:read`
36
+
37
+ ---
38
+
39
+ ## Available Tools
40
+
41
+ | Tool | Answers |
42
+ |------|---------|
43
+ | `list-sites` | What sites are in my account? |
44
+ | `get-traffic-overview` | How is my traffic doing? |
45
+ | `get-top-pages` | Which pages get the most traffic? |
46
+ | `get-referrers` | Where does my traffic come from? |
47
+ | `get-realtime` | Who is on my site right now? |
48
+ | `get-goals` | What are my conversion rates? |
49
+ | `get-funnels` | Where do users drop off? |
50
+ | `get-releases` | Did my last release affect traffic? |
51
+ | `get-seo-suggestions` | What should I fix for SEO? |
52
+ | `analyze-traffic-drop` | Why did traffic drop? |
53
+
54
+ ---
55
+
56
+ ## Client Setup
57
+
58
+ ### Claude Desktop
59
+
60
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "growtics": {
66
+ "command": "npx",
67
+ "args": ["-y", "growtics-mcp"],
68
+ "env": {
69
+ "GROWTICS_API_KEY": "gt_live_YOUR_KEY_HERE"
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ Restart Claude Desktop. Look for the 🔨 tools icon to confirm it connected.
77
+
78
+ ### Cursor
79
+
80
+ Edit `.cursor/mcp.json` in your project root:
81
+
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "growtics": {
86
+ "command": "npx",
87
+ "args": ["-y", "growtics-mcp"],
88
+ "env": {
89
+ "GROWTICS_API_KEY": "gt_live_YOUR_KEY_HERE"
90
+ }
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ ### Windsurf
97
+
98
+ Edit `~/.windsurf/mcp.json`:
99
+
100
+ ```json
101
+ {
102
+ "mcpServers": {
103
+ "growtics": {
104
+ "command": "npx",
105
+ "args": ["-y", "growtics-mcp"],
106
+ "env": {
107
+ "GROWTICS_API_KEY": "gt_live_YOUR_KEY_HERE"
108
+ }
109
+ }
110
+ }
111
+ }
112
+ ```
113
+
114
+ ### Any `.mcp.json`
115
+
116
+ Drop this at the root of any project:
117
+
118
+ ```json
119
+ {
120
+ "mcpServers": {
121
+ "growtics": {
122
+ "command": "npx",
123
+ "args": ["-y", "growtics-mcp"],
124
+ "env": {
125
+ "GROWTICS_API_KEY": "gt_live_YOUR_KEY_HERE"
126
+ }
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Environment Variables
135
+
136
+ | Variable | Required | Default | Description |
137
+ |----------|----------|---------|-------------|
138
+ | `GROWTICS_API_KEY` | ✅ Yes | — | Your Growtics API key (`gt_live_...`) |
139
+ | `GROWTICS_API_URL` | No | `https://growtics.io` | Override for self-hosted Growtics |
140
+
141
+ ---
142
+
143
+ ## Example Conversation
144
+
145
+ ```
146
+ You: Why did traffic drop this week on my site?
147
+
148
+ Claude: [calls analyze-traffic-drop]
149
+
150
+ Traffic is down 23% vs the previous 7-day period.
151
+
152
+ Key findings:
153
+ 1. Organic search dropped 41% — Google traffic fell significantly
154
+ 2. Twitter/X referrals are down 60%
155
+ 3. The /pricing page saw a 35% decline in the same period
156
+
157
+ Recommended actions:
158
+ 1. Check Google Search Console for crawl issues or ranking changes
159
+ 2. Review recent posts on X — a high-traffic post from last week
160
+ is no longer sending referrals
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Security
166
+
167
+ - **Read-only** — the MCP server can only query data, never modify it
168
+ - **Local process** — runs on your machine; data goes directly to `growtics.io`, not through any third party
169
+ - **Scoped keys** — create a dedicated read-only API key for MCP usage
170
+
171
+ ---
172
+
173
+ ## Links
174
+
175
+ - [Growtics](https://growtics.io) — Website growth analytics
176
+ - [Full Setup Guide](https://growtics.io/docs/mcp)
177
+ - [MCP Protocol](https://modelcontextprotocol.io)
178
+
179
+ ## License
180
+
181
+ MIT
package/index.js ADDED
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Growtics Analytics MCP Server
4
+ *
5
+ * Exposes your Growtics analytics data as MCP tools consumable by
6
+ * Claude Desktop, Cursor, Windsurf, and any other MCP-compatible AI assistant.
7
+ *
8
+ * Usage:
9
+ * GROWTICS_API_KEY=gt_live_... GROWTICS_API_URL=https://growtics.io node index.js
10
+ *
11
+ * Or configure in your AI client's MCP settings (see MCP_GUIDE.md).
12
+ */
13
+
14
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import {
17
+ CallToolRequestSchema,
18
+ ListToolsRequestSchema,
19
+ } from '@modelcontextprotocol/sdk/types.js';
20
+
21
+ // ── Tool modules ──────────────────────────────────────────────────────────────
22
+ import * as listSites from './tools/list-sites.js';
23
+ import * as getTrafficOverview from './tools/get-traffic-overview.js';
24
+ import * as getTopPages from './tools/get-top-pages.js';
25
+ import * as getReferrers from './tools/get-referrers.js';
26
+ import * as getRealtime from './tools/get-realtime.js';
27
+ import * as getGoals from './tools/get-goals.js';
28
+ import * as getFunnels from './tools/get-funnels.js';
29
+ import * as getReleases from './tools/get-releases.js';
30
+ import * as getSeoSuggestions from './tools/get-seo-suggestions.js';
31
+ import * as analyzeTrafficDrop from './tools/analyze-traffic-drop.js';
32
+
33
+ // ── Configuration ─────────────────────────────────────────────────────────────
34
+ const API_KEY = process.env.GROWTICS_API_KEY;
35
+ const API_URL = (process.env.GROWTICS_API_URL || 'https://growtics.io').replace(/\/$/, '');
36
+
37
+ if (!API_KEY) {
38
+ process.stderr.write(
39
+ '[growtics-mcp] ERROR: GROWTICS_API_KEY environment variable is required.\n' +
40
+ 'Get your API key from: Settings → API Keys in the Growtics dashboard.\n'
41
+ );
42
+ process.exit(1);
43
+ }
44
+
45
+ // ── API Client ────────────────────────────────────────────────────────────────
46
+ const apiClient = {
47
+ async get(path) {
48
+ const url = `${API_URL}${path}`;
49
+ const response = await fetch(url, {
50
+ headers: {
51
+ Authorization: `Bearer ${API_KEY}`,
52
+ 'Content-Type': 'application/json',
53
+ 'User-Agent': 'growtics-mcp/1.0.0',
54
+ },
55
+ });
56
+
57
+ if (!response.ok) {
58
+ let errMsg = `HTTP ${response.status}`;
59
+ try {
60
+ const body = await response.json();
61
+ errMsg = body?.error?.message || body?.error || errMsg;
62
+ } catch {}
63
+ throw new Error(`Growtics API error: ${errMsg}`);
64
+ }
65
+
66
+ return response.json();
67
+ },
68
+ };
69
+
70
+ // ── Tool Registry ─────────────────────────────────────────────────────────────
71
+ const TOOLS = [
72
+ listSites,
73
+ getTrafficOverview,
74
+ getTopPages,
75
+ getReferrers,
76
+ getRealtime,
77
+ getGoals,
78
+ getFunnels,
79
+ getReleases,
80
+ getSeoSuggestions,
81
+ analyzeTrafficDrop,
82
+ ];
83
+
84
+ const TOOL_MAP = Object.fromEntries(TOOLS.map(t => [t.definition.name, t]));
85
+
86
+ // ── MCP Server ────────────────────────────────────────────────────────────────
87
+ const server = new Server(
88
+ {
89
+ name: 'growtics-mcp',
90
+ version: '1.0.0',
91
+ },
92
+ {
93
+ capabilities: { tools: {} },
94
+ }
95
+ );
96
+
97
+ // List available tools
98
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
99
+ tools: TOOLS.map(t => ({
100
+ name: t.definition.name,
101
+ description: t.definition.description,
102
+ inputSchema: t.definition.inputSchema,
103
+ })),
104
+ }));
105
+
106
+ // Handle tool calls
107
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
108
+ const { name, arguments: args } = request.params;
109
+
110
+ const tool = TOOL_MAP[name];
111
+ if (!tool) {
112
+ return {
113
+ content: [{ type: 'text', text: `Unknown tool: "${name}"` }],
114
+ isError: true,
115
+ };
116
+ }
117
+
118
+ try {
119
+ const result = await tool.run({ args: args || {}, apiClient });
120
+ return {
121
+ content: [{ type: 'text', text: result.text }],
122
+ };
123
+ } catch (err) {
124
+ const message = err?.message || 'An unexpected error occurred.';
125
+ process.stderr.write(`[growtics-mcp] Tool "${name}" error: ${message}\n`);
126
+ return {
127
+ content: [
128
+ {
129
+ type: 'text',
130
+ text: `❌ Error running \`${name}\`:\n\n${message}\n\nMake sure your API key has the correct scopes and the siteId is valid.`,
131
+ },
132
+ ],
133
+ isError: true,
134
+ };
135
+ }
136
+ });
137
+
138
+ // ── Start ─────────────────────────────────────────────────────────────────────
139
+ async function main() {
140
+ const transport = new StdioServerTransport();
141
+ await server.connect(transport);
142
+ process.stderr.write(`[growtics-mcp] Server started. Connected to ${API_URL}\n`);
143
+ }
144
+
145
+ main().catch((err) => {
146
+ process.stderr.write(`[growtics-mcp] Fatal error: ${err.message}\n`);
147
+ process.exit(1);
148
+ });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "growtics-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Growtics Analytics — ask your AI assistant questions about your website traffic, SEO, conversions, and releases.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "growtics-mcp": "index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "tools/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "start": "node index.js",
18
+ "prepublishOnly": "node --check index.js && echo '✅ Syntax OK'"
19
+ },
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.12.0"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "model-context-protocol",
29
+ "growtics",
30
+ "analytics",
31
+ "ai",
32
+ "claude",
33
+ "cursor",
34
+ "windsurf",
35
+ "seo",
36
+ "web-analytics"
37
+ ],
38
+ "license": "MIT",
39
+ "homepage": "https://growtics.io",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/growtics/growtics-mcp.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/growtics/growtics-mcp/issues"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * analyze-traffic-drop tool
3
+ * Smart composite analysis: compares current vs previous period to surface
4
+ * traffic drop causes across multiple dimensions simultaneously.
5
+ */
6
+ export const definition = {
7
+ name: 'analyze-traffic-drop',
8
+ description:
9
+ 'Perform a comprehensive traffic drop analysis by comparing the current period against the ' +
10
+ 'previous equivalent period. Surfaces the root cause across traffic volume, referral sources, ' +
11
+ 'top pages, devices, and countries — all in one intelligent report. ' +
12
+ 'Use this to answer "Why did traffic drop this week?" or "What changed vs last month?"',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ siteId: {
17
+ type: 'string',
18
+ description: 'The Growtics site UUID (get it from list-sites)',
19
+ },
20
+ period: {
21
+ type: 'string',
22
+ enum: ['7d', '28d', 'mtd', 'lastmonth', '91d', '12m'],
23
+ description:
24
+ 'Current period to analyze. The tool will automatically compare it against ' +
25
+ 'the equivalent previous period. Default: 7d',
26
+ },
27
+ },
28
+ required: ['siteId'],
29
+ },
30
+ };
31
+
32
+ // Map a period to the equivalent "previous" period for comparison
33
+ const COMPARE_PERIODS = {
34
+ '7d': ['7d', '28d'], // current 7d vs prev 7d (use 28d, take second half)
35
+ '28d': ['28d', '91d'],
36
+ 'mtd': ['mtd', 'lastmonth'],
37
+ 'lastmonth': ['lastmonth', '28d'],
38
+ '91d': ['91d', '12m'],
39
+ '12m': ['12m', 'all'],
40
+ };
41
+
42
+ export async function run({ args, apiClient }) {
43
+ const { siteId, period = '7d' } = args;
44
+
45
+ // Fetch current and previous period overviews in parallel
46
+ const [current, previous, referrersCurrent, referrersPrev, goalsCurrent] = await Promise.all([
47
+ apiClient.get(`/api/v1/analytics?siteId=${siteId}&period=${period}&type=overview`),
48
+ apiClient.get(`/api/v1/analytics?siteId=${siteId}&period=${COMPARE_PERIODS[period]?.[1] || '28d'}&type=overview`),
49
+ apiClient.get(`/api/v1/analytics?siteId=${siteId}&period=${period}&type=referrals`),
50
+ apiClient.get(`/api/v1/analytics?siteId=${siteId}&period=${COMPARE_PERIODS[period]?.[1] || '28d'}&type=referrals`),
51
+ apiClient.get(`/api/v1/analytics?siteId=${siteId}&period=${period}&type=goals`).catch(() => null),
52
+ ]);
53
+
54
+ const meta = current.meta;
55
+ const cur = current.data.stats;
56
+ const prev = previous.data.stats;
57
+
58
+ // Core metric deltas
59
+ const viewsDelta = prev.pageviews > 0 ? Math.round(((cur.pageviews - prev.pageviews) / prev.pageviews) * 100) : null;
60
+ const visitorsDelta = prev.uniqueVisitors > 0 ? Math.round(((cur.uniqueVisitors - prev.uniqueVisitors) / prev.uniqueVisitors) * 100) : null;
61
+ const bounceDelta = (cur.bounceRate - prev.bounceRate).toFixed(1);
62
+
63
+ function arrow(delta, higherIsBad = false) {
64
+ if (delta === null) return '—';
65
+ const isGood = higherIsBad ? delta < 0 : delta > 0;
66
+ const icon = isGood ? '🟢' : delta === 0 ? '⚪' : '🔴';
67
+ const ar = delta > 0 ? '▲' : delta < 0 ? '▼' : '→';
68
+ return `${icon} ${ar} ${Math.abs(delta)}%`;
69
+ }
70
+
71
+ const lines = [
72
+ `## Traffic Analysis — ${meta.siteName} (${meta.domain})`,
73
+ `**Comparing:** \`${period}\` (current) vs \`${COMPARE_PERIODS[period]?.[1] || '28d'}\` (previous)\n`,
74
+
75
+ `### 📊 Core Metrics Comparison`,
76
+ `| Metric | Current | Previous | Change |`,
77
+ `|--------|---------|----------|--------|`,
78
+ `| Page Views | **${cur.pageviews.toLocaleString()}** | ${prev.pageviews.toLocaleString()} | ${arrow(viewsDelta)} |`,
79
+ `| Unique Visitors | **${cur.uniqueVisitors.toLocaleString()}** | ${prev.uniqueVisitors.toLocaleString()} | ${arrow(visitorsDelta)} |`,
80
+ `| Bounce Rate | **${cur.bounceRate}%** | ${prev.bounceRate}% | ${parseFloat(bounceDelta) > 2 ? '🔴' : parseFloat(bounceDelta) < -2 ? '🟢' : '⚪'} ${bounceDelta > 0 ? '+' : ''}${bounceDelta}pp |`,
81
+ `| Active Now | **${cur.activeVisitors}** | — | — |`,
82
+ ];
83
+
84
+ // Overall verdict
85
+ const verdict = viewsDelta === null ? null :
86
+ viewsDelta <= -20 ? '🚨 **Significant drop** detected — traffic is materially lower than the previous period.' :
87
+ viewsDelta <= -10 ? '⚠️ **Moderate drop** detected — worth investigating the sources below.' :
88
+ viewsDelta <= -5 ? '📉 **Slight decline** — minor movement, possibly normal variance.' :
89
+ viewsDelta >= 10 ? '🚀 **Strong growth** — traffic is up significantly!' :
90
+ '✅ Traffic is relatively stable compared to the previous period.';
91
+
92
+ if (verdict) lines.push(`\n> ${verdict}`);
93
+
94
+ // ── Referrer source analysis ──────────────────────────────────────────────
95
+ lines.push('\n### 🔗 Traffic Source Changes');
96
+
97
+ const curRefs = Object.fromEntries((referrersCurrent.data?.referrers || []).map(r => [r.domain, r.pageviews]));
98
+ const prevRefs = Object.fromEntries((referrersPrev.data?.referrers || []).map(r => [r.domain, r.pageviews]));
99
+ const allDomains = new Set([...Object.keys(curRefs), ...Object.keys(prevRefs)]);
100
+
101
+ const refChanges = [...allDomains].map(domain => {
102
+ const c = curRefs[domain] || 0;
103
+ const p = prevRefs[domain] || 0;
104
+ const delta = p > 0 ? Math.round(((c - p) / p) * 100) : (c > 0 ? 100 : 0);
105
+ return { domain, current: c, previous: p, delta };
106
+ }).sort((a, b) => (a.delta - b.delta)); // most dropped first
107
+
108
+ const bigDroppers = refChanges.filter(r => r.delta < -20 && r.previous > 10).slice(0, 5);
109
+ const bigGainers = refChanges.filter(r => r.delta > 20 && r.previous > 0).slice(0, 3);
110
+ const newSources = refChanges.filter(r => r.previous === 0 && r.current > 5).slice(0, 3);
111
+
112
+ if (bigDroppers.length > 0) {
113
+ lines.push('\n**📉 Sources that dropped significantly:**');
114
+ lines.push('| Source | Current | Previous | Change |');
115
+ lines.push('|--------|---------|----------|--------|');
116
+ for (const r of bigDroppers) {
117
+ lines.push(`| **${r.domain || '(direct)'}** | ${r.current.toLocaleString()} | ${r.previous.toLocaleString()} | 🔴 ▼ ${Math.abs(r.delta)}% |`);
118
+ }
119
+ }
120
+
121
+ if (bigGainers.length > 0) {
122
+ lines.push('\n**📈 Sources that grew:**');
123
+ for (const r of bigGainers) {
124
+ lines.push(`- **${r.domain}**: ${r.previous.toLocaleString()} → ${r.current.toLocaleString()} (🟢 ▲ ${r.delta}%)`);
125
+ }
126
+ }
127
+
128
+ if (newSources.length > 0) {
129
+ lines.push('\n**🆕 New traffic sources this period:**');
130
+ for (const r of newSources) {
131
+ lines.push(`- **${r.domain}**: ${r.current.toLocaleString()} visits (new)`);
132
+ }
133
+ }
134
+
135
+ // ── Page-level changes ───────────────────────────────────────────────────
136
+ const curPages = current.data.topPages || [];
137
+ const prevPages = previous.data.topPages || [];
138
+
139
+ if (curPages.length > 0) {
140
+ lines.push('\n### 📄 Top Pages — Current Period');
141
+ lines.push('| Page | Views |');
142
+ lines.push('|------|-------|');
143
+ curPages.slice(0, 5).forEach(p => {
144
+ const prev = prevPages.find(pp => pp.path === p.path);
145
+ const delta = prev ? Math.round(((p.pageviews - prev.pageviews) / prev.pageviews) * 100) : null;
146
+ const changeStr = delta !== null ? (delta > 0 ? ` 🟢▲${delta}%` : delta < 0 ? ` 🔴▼${Math.abs(delta)}%` : '') : ' 🆕';
147
+ lines.push(`| \`${p.path}\` | ${p.pageviews.toLocaleString()}${changeStr} |`);
148
+ });
149
+ }
150
+
151
+ // ── Device breakdown ─────────────────────────────────────────────────────
152
+ const curDevices = current.data.topDevices || [];
153
+ const prevDevices = previous.data.topDevices || [];
154
+ if (curDevices.length > 0) {
155
+ lines.push('\n### 📱 Device Breakdown');
156
+ lines.push('| Device | Current | Previous | Change |');
157
+ lines.push('|--------|---------|----------|--------|');
158
+ for (const d of curDevices) {
159
+ const p = prevDevices.find(x => x.device === d.device);
160
+ const pd = p?.pageviews || 0;
161
+ const delta = pd > 0 ? Math.round(((d.pageviews - pd) / pd) * 100) : null;
162
+ const changeStr = delta !== null ? (delta > 0 ? `🟢 ▲ ${delta}%` : delta < 0 ? `🔴 ▼ ${Math.abs(delta)}%` : '⚪ flat') : '🆕 new';
163
+ lines.push(`| ${d.device} | ${d.pageviews.toLocaleString()} | ${pd.toLocaleString()} | ${changeStr} |`);
164
+ }
165
+ }
166
+
167
+ // ── Goal conversions ─────────────────────────────────────────────────────
168
+ if (goalsCurrent?.data?.goals?.length > 0) {
169
+ lines.push('\n### 🎯 Conversion Goals');
170
+ lines.push('| Goal | Completions | Conv. Rate |');
171
+ lines.push('|------|-------------|------------|');
172
+ for (const g of goalsCurrent.data.goals) {
173
+ const rate = parseFloat(g.conversionRate);
174
+ const health = rate >= 5 ? '🟢' : rate >= 1 ? '🟡' : '🔴';
175
+ lines.push(`| **${g.name}** | ${g.completions.toLocaleString()} | ${health} ${rate}% |`);
176
+ }
177
+ }
178
+
179
+ // ── AI Summary & Recommendations ─────────────────────────────────────────
180
+ lines.push('\n---\n### 🤖 Analysis Summary');
181
+
182
+ const findings = [];
183
+
184
+ if (viewsDelta !== null && viewsDelta <= -10) {
185
+ findings.push(`Traffic dropped **${Math.abs(viewsDelta)}%** compared to the previous period.`);
186
+ }
187
+ if (bigDroppers.length > 0) {
188
+ findings.push(`The biggest source drops came from: ${bigDroppers.map(r => `**${r.domain || '(direct)'}** (−${Math.abs(r.delta)}%)`).join(', ')}.`);
189
+ }
190
+ if (parseFloat(bounceDelta) > 5) {
191
+ findings.push(`Bounce rate increased by **${bounceDelta} percentage points** — visitors may not be finding what they expect on landing.`);
192
+ }
193
+ if (curDevices.find(d => d.device === 'mobile')?.pageviews < (prevDevices.find(d => d.device === 'mobile')?.pageviews || 0)) {
194
+ findings.push('Mobile traffic in particular has declined — check mobile page speed and UX.');
195
+ }
196
+ if (newSources.length > 0) {
197
+ findings.push(`New traffic sources appeared: ${newSources.map(r => `**${r.domain}**`).join(', ')}.`);
198
+ }
199
+
200
+ if (findings.length === 0 && (viewsDelta === null || Math.abs(viewsDelta) < 5)) {
201
+ findings.push('Traffic metrics are stable. No significant drops or anomalies detected in this period.');
202
+ }
203
+
204
+ findings.forEach((f, i) => lines.push(`${i + 1}. ${f}`));
205
+
206
+ // Recommendations
207
+ const recs = [];
208
+ if (bigDroppers.some(r => /google\.|bing\.com|yahoo\.com|duckduckgo/.test(r.domain || ''))) {
209
+ recs.push('Organic search dropped — check Google Search Console for crawl issues or ranking changes. Review open SEO suggestions in Growtics.');
210
+ }
211
+ if (bigDroppers.some(r => /t\.co|twitter\.com|x\.com|facebook\.com|instagram\.com|linkedin\.com/.test(r.domain || ''))) {
212
+ recs.push('Social traffic dropped — check if recent social posts underperformed or if a high-traffic post from the previous period is no longer driving clicks.');
213
+ }
214
+ if (parseFloat(bounceDelta) > 5) {
215
+ recs.push('High bounce rate increase — review the top landing pages for content relevance and page speed.');
216
+ }
217
+ if (recs.length === 0 && viewsDelta !== null && viewsDelta < -10) {
218
+ recs.push('Run `get-seo-suggestions` for actionable improvement ideas.');
219
+ recs.push('Check `get-releases` to see if any recent change correlated with the drop.');
220
+ }
221
+
222
+ if (recs.length > 0) {
223
+ lines.push('\n**🔧 Recommended Actions:**');
224
+ recs.forEach((r, i) => lines.push(`${i + 1}. ${r}`));
225
+ }
226
+
227
+ return { text: lines.join('\n') };
228
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * get-funnels tool
3
+ * Runs a funnel analysis for a set of page paths.
4
+ */
5
+ export const definition = {
6
+ name: 'get-funnels',
7
+ description:
8
+ 'Analyze a conversion funnel: given a sequence of page paths, calculate how many visitors ' +
9
+ 'progress through each step. Use this to identify where visitors drop off. ' +
10
+ 'Example: "/,/pricing,/signup,/dashboard" shows the homepage-to-dashboard signup funnel.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ siteId: {
15
+ type: 'string',
16
+ description: 'The Growtics site UUID (get it from list-sites)',
17
+ },
18
+ steps: {
19
+ type: 'string',
20
+ description:
21
+ 'Comma-separated list of page paths defining the funnel steps. ' +
22
+ 'Minimum 2, maximum 10. Example: "/,/pricing,/signup"',
23
+ },
24
+ period: {
25
+ type: 'string',
26
+ enum: ['today', 'yesterday', '24h', '7d', '28d', '91d', 'mtd', 'lastmonth', 'ytd', '12m', 'all'],
27
+ description: 'Time period for the report. Default: 7d',
28
+ },
29
+ },
30
+ required: ['siteId', 'steps'],
31
+ },
32
+ };
33
+
34
+ export async function run({ args, apiClient }) {
35
+ const { siteId, steps, period = '7d' } = args;
36
+ const data = await apiClient.get(
37
+ `/api/v1/analytics?siteId=${siteId}&period=${period}&type=funnels&steps=${encodeURIComponent(steps)}`
38
+ );
39
+
40
+ const { steps: funnelSteps } = data.data;
41
+ const meta = data.meta;
42
+
43
+ if (!funnelSteps?.length) {
44
+ return { text: `No funnel data found for the given steps on **${meta.siteName}**.` };
45
+ }
46
+
47
+ const topCount = funnelSteps[0]?.visitors || 0;
48
+
49
+ const lines = [
50
+ `## Funnel Analysis — ${meta.siteName}`,
51
+ `**Period:** ${period} | **Steps:** ${steps}\n`,
52
+ `| Step | Page | Visitors | Drop-off | Conv. Rate |`,
53
+ `|------|------|----------|----------|------------|`,
54
+ ];
55
+
56
+ for (let i = 0; i < funnelSteps.length; i++) {
57
+ const step = funnelSteps[i];
58
+ const prev = i > 0 ? funnelSteps[i - 1].visitors : step.visitors;
59
+ const dropoff = prev > 0 ? Math.round(((prev - step.visitors) / prev) * 100) : 0;
60
+ const overallRate = topCount > 0 ? ((step.visitors / topCount) * 100).toFixed(1) : '0.0';
61
+ const dropoffStr = i === 0 ? '—' : `${dropoff}% lost`;
62
+ const emoji = i === 0 ? '' : dropoff > 50 ? ' 🔴' : dropoff > 25 ? ' 🟡' : ' 🟢';
63
+
64
+ lines.push(
65
+ `| ${step.step} | \`${step.path}\` | ${step.visitors.toLocaleString()} | ${dropoffStr}${emoji} | ${overallRate}% |`
66
+ );
67
+ }
68
+
69
+ // Find the biggest drop
70
+ let maxDrop = 0, maxDropStep = null;
71
+ for (let i = 1; i < funnelSteps.length; i++) {
72
+ const prev = funnelSteps[i - 1].visitors;
73
+ const curr = funnelSteps[i].visitors;
74
+ const drop = prev > 0 ? ((prev - curr) / prev) * 100 : 0;
75
+ if (drop > maxDrop) { maxDrop = drop; maxDropStep = funnelSteps[i]; }
76
+ }
77
+
78
+ if (maxDropStep && maxDrop > 20) {
79
+ lines.push(
80
+ `\n> 🚨 **Biggest drop-off point:** Step ${maxDropStep.step} (\`${maxDropStep.path}\`) — **${Math.round(maxDrop)}% of visitors** leave at this step. ` +
81
+ `This is the highest-impact place to optimize.`
82
+ );
83
+ }
84
+
85
+ return { text: lines.join('\n') };
86
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * get-goals tool
3
+ * Returns conversion goals and their completion rates.
4
+ */
5
+ export const definition = {
6
+ name: 'get-goals',
7
+ description:
8
+ 'Get all conversion goals configured for a site and their completion rates for a period. ' +
9
+ 'Use this to answer "Which pages are losing conversions?", ' +
10
+ '"What is my signup conversion rate?", or "Are my goals performing well?"',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ siteId: {
15
+ type: 'string',
16
+ description: 'The Growtics site UUID (get it from list-sites)',
17
+ },
18
+ period: {
19
+ type: 'string',
20
+ enum: ['today', 'yesterday', '24h', '7d', '28d', '91d', 'mtd', 'lastmonth', 'ytd', '12m', 'all'],
21
+ description: 'Time period for the report. Default: 7d',
22
+ },
23
+ },
24
+ required: ['siteId'],
25
+ },
26
+ };
27
+
28
+ export async function run({ args, apiClient }) {
29
+ const { siteId, period = '7d' } = args;
30
+ const data = await apiClient.get(`/api/v1/analytics?siteId=${siteId}&period=${period}&type=goals`);
31
+
32
+ const { baseline, goals } = data.data;
33
+ const meta = data.meta;
34
+
35
+ if (!goals?.length) {
36
+ return {
37
+ text:
38
+ `## Conversion Goals — ${meta.siteName}\n\n` +
39
+ `No goals are configured for this site yet.\n` +
40
+ `Add goals in the Growtics dashboard under **Analytics → Goals**.`,
41
+ };
42
+ }
43
+
44
+ // Sort by conversion rate desc
45
+ const sorted = [...goals].sort((a, b) => b.conversionRate - a.conversionRate);
46
+
47
+ const lines = [
48
+ `## Conversion Goals — ${meta.siteName}`,
49
+ `**Period:** ${period} | **Total unique visitors (baseline):** ${baseline.toLocaleString()}\n`,
50
+ `| Goal | Target | Completions | Conv. Rate | Health |`,
51
+ `|------|--------|-------------|------------|--------|`,
52
+ ...sorted.map(g => {
53
+ const rate = parseFloat(g.conversionRate);
54
+ const health = rate >= 5 ? '🟢 Good' : rate >= 1 ? '🟡 Average' : '🔴 Low';
55
+ return `| **${g.name}** | \`${g.targetPath}\` | ${g.completions.toLocaleString()} | **${rate}%** | ${health} |`;
56
+ }),
57
+ ];
58
+
59
+ // Highlight worst performer
60
+ const worst = sorted[sorted.length - 1];
61
+ if (worst && parseFloat(worst.conversionRate) < 1) {
62
+ lines.push(
63
+ `\n> ⚠️ **"${worst.name}"** has a very low conversion rate of **${worst.conversionRate}%**. ` +
64
+ `Consider reviewing the page \`${worst.targetPath}\` for UX or copy improvements.`
65
+ );
66
+ }
67
+
68
+ return { text: lines.join('\n') };
69
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * get-realtime tool
3
+ * Returns currently active visitors and recent pageviews (last 5 minutes).
4
+ */
5
+ export const definition = {
6
+ name: 'get-realtime',
7
+ description:
8
+ 'Get real-time data: how many visitors are on your site right now, ' +
9
+ 'which pages they are viewing, and their geographic breakdown. ' +
10
+ 'Use this to monitor live events, campaigns, or to check if a page is being visited.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ siteId: {
15
+ type: 'string',
16
+ description: 'The Growtics site UUID (get it from list-sites)',
17
+ },
18
+ },
19
+ required: ['siteId'],
20
+ },
21
+ };
22
+
23
+ export async function run({ args, apiClient }) {
24
+ const { siteId } = args;
25
+ const data = await apiClient.get(`/api/v1/analytics?siteId=${siteId}&type=realtime`);
26
+
27
+ const { activeVisitors, recentPageviews, countryBreakdown } = data.data;
28
+ const meta = data.meta;
29
+
30
+ const lines = [
31
+ `## Real-Time — ${meta.siteName}`,
32
+ `*Snapshot at ${new Date().toLocaleTimeString()}*\n`,
33
+ `### 🟢 ${activeVisitors} Active Visitor${activeVisitors !== 1 ? 's' : ''} (last 5 min)\n`,
34
+ ];
35
+
36
+ if (recentPageviews.length > 0) {
37
+ lines.push('### Recent Pageviews');
38
+ lines.push('| Time | Page | Country | Device |');
39
+ lines.push('|------|------|---------|--------|');
40
+ for (const pv of recentPageviews.slice(0, 10)) {
41
+ const time = new Date(pv.timestamp).toLocaleTimeString();
42
+ lines.push(`| ${time} | \`${pv.path}\` | ${pv.country || '—'} | ${pv.device || '—'} |`);
43
+ }
44
+ } else {
45
+ lines.push('_No pageviews in the last 5 minutes._');
46
+ }
47
+
48
+ if (countryBreakdown.length > 0) {
49
+ lines.push('\n### Visitors by Country (last 30 min)');
50
+ lines.push(
51
+ countryBreakdown.slice(0, 8)
52
+ .map(c => `- **${c.country || 'Unknown'}**: ${c.count} visitor${c.count !== 1 ? 's' : ''}`)
53
+ .join('\n')
54
+ );
55
+ }
56
+
57
+ return { text: lines.join('\n') };
58
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * get-referrers tool
3
+ * Returns traffic referrer sources for a given period.
4
+ */
5
+ export const definition = {
6
+ name: 'get-referrers',
7
+ description:
8
+ 'Get the top traffic sources (referrers) for your website — which domains, ' +
9
+ 'search engines, or social networks are sending you visitors. ' +
10
+ 'Use this to answer "Where is my traffic coming from?" or ' +
11
+ '"Which channel dropped this week?"',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ siteId: {
16
+ type: 'string',
17
+ description: 'The Growtics site UUID (get it from list-sites)',
18
+ },
19
+ period: {
20
+ type: 'string',
21
+ enum: ['today', 'yesterday', '24h', '7d', '28d', '91d', 'mtd', 'lastmonth', 'ytd', '12m', 'all'],
22
+ description: 'Time period for the report. Default: 7d',
23
+ },
24
+ },
25
+ required: ['siteId'],
26
+ },
27
+ };
28
+
29
+ // Simple channel classification from a referrer domain
30
+ function classifyChannel(domain) {
31
+ if (!domain) return 'Direct / Unknown';
32
+ const d = domain.toLowerCase();
33
+ if (/google\.|bing\.com|yahoo\.com|duckduckgo|yandex\.|baidu\.com|ecosia\./.test(d))
34
+ return '🔍 Organic Search';
35
+ if (/t\.co|twitter\.com|x\.com|facebook\.com|instagram\.com|linkedin\.com|youtube\.com|pinterest\.com|reddit\.com|tiktok\.com/.test(d))
36
+ return '📱 Organic Social';
37
+ if (/chatgpt\.com|openai\.com|claude\.ai|anthropic\.com|gemini\.google|perplexity\.ai/.test(d))
38
+ return '🤖 AI / LLM';
39
+ return '🔗 Referral';
40
+ }
41
+
42
+ export async function run({ args, apiClient }) {
43
+ const { siteId, period = '7d' } = args;
44
+ const data = await apiClient.get(`/api/v1/analytics?siteId=${siteId}&period=${period}&type=referrals`);
45
+
46
+ const { referrers } = data.data;
47
+ const meta = data.meta;
48
+
49
+ if (!referrers?.length) {
50
+ return { text: `No referrer data found for **${meta.siteName}** in the period \`${period}\`.` };
51
+ }
52
+
53
+ const total = referrers.reduce((s, r) => s + r.pageviews, 0);
54
+
55
+ // Group by channel
56
+ const channels = {};
57
+ for (const r of referrers) {
58
+ const ch = classifyChannel(r.domain);
59
+ if (!channels[ch]) channels[ch] = { pageviews: 0, domains: [] };
60
+ channels[ch].pageviews += r.pageviews;
61
+ channels[ch].domains.push(r);
62
+ }
63
+
64
+ const lines = [
65
+ `## Traffic Sources — ${meta.siteName}`,
66
+ `**Period:** ${period} | **Total referred visits:** ${total.toLocaleString()}\n`,
67
+ ];
68
+
69
+ // Channel summary
70
+ const sortedChannels = Object.entries(channels).sort((a, b) => b[1].pageviews - a[1].pageviews);
71
+ lines.push('### By Channel');
72
+ lines.push('| Channel | Views | Share |');
73
+ lines.push('|---------|-------|-------|');
74
+ for (const [ch, info] of sortedChannels) {
75
+ const share = total > 0 ? ((info.pageviews / total) * 100).toFixed(1) : '0.0';
76
+ lines.push(`| ${ch} | ${info.pageviews.toLocaleString()} | ${share}% |`);
77
+ }
78
+
79
+ // Top individual referrers
80
+ lines.push('\n### Top Individual Sources');
81
+ lines.push('| # | Domain | Views | Channel |');
82
+ lines.push('|---|--------|-------|---------|');
83
+ referrers.slice(0, 15).forEach((r, i) => {
84
+ const share = total > 0 ? ((r.pageviews / total) * 100).toFixed(1) : '0.0';
85
+ lines.push(`| ${i + 1} | **${r.domain || '(direct)'}** | ${r.pageviews.toLocaleString()} (${share}%) | ${classifyChannel(r.domain)} |`);
86
+ });
87
+
88
+ return { text: lines.join('\n') };
89
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * get-releases tool
3
+ * Returns recent releases with matched-window pre/post impact metrics.
4
+ */
5
+ export const definition = {
6
+ name: 'get-releases',
7
+ description:
8
+ 'Get your recent website releases (features, bug fixes, marketing changes) with ' +
9
+ 'matched-window pre/post analytics: traffic, visitor, revenue, and goal completion changes ' +
10
+ 'before vs after each release. ' +
11
+ 'Use this to answer "What impact did my last release have?" or ' +
12
+ '"Did the new feature increase signups?"',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ siteId: {
17
+ type: 'string',
18
+ description: 'The Growtics site UUID (get it from list-sites)',
19
+ },
20
+ period: {
21
+ type: 'string',
22
+ enum: ['24h', '7d', '14d', '30d'],
23
+ description: 'Comparison window size. Default: 7d',
24
+ },
25
+ limit: {
26
+ type: 'number',
27
+ description: 'Maximum number of releases to return (default: 5, max: 50)',
28
+ },
29
+ },
30
+ required: ['siteId'],
31
+ },
32
+ };
33
+
34
+ function pct(pre, post) {
35
+ if (pre === 0 && post === 0) return null;
36
+ if (pre === 0) return null;
37
+ return Math.round(((post - pre) / pre) * 100);
38
+ }
39
+
40
+ function formatChange(pre, post, unit = '') {
41
+ const change = pct(pre, post);
42
+ if (change === null) return '—';
43
+ const arrow = change > 0 ? '▲' : change < 0 ? '▼' : '→';
44
+ const color = change > 0 ? '🟢' : change < 0 ? '🔴' : '⚪';
45
+ return `${color} ${arrow} ${Math.abs(change)}%${unit}`;
46
+ }
47
+
48
+ export async function run({ args, apiClient }) {
49
+ const { siteId, period = '7d', limit = 5 } = args;
50
+ const data = await apiClient.get(
51
+ `/api/v1/releases?siteId=${siteId}&period=${period}&limit=${limit}`
52
+ );
53
+
54
+ const { releases } = data.data;
55
+ const meta = data.meta;
56
+
57
+ if (!releases?.length) {
58
+ return {
59
+ text:
60
+ `## Releases — ${meta.siteName}\n\n` +
61
+ `No releases logged yet.\n` +
62
+ `Log releases in the Growtics dashboard to track their impact.`,
63
+ };
64
+ }
65
+
66
+ const lines = [
67
+ `## Release Impact Analysis — ${meta.siteName}`,
68
+ `**Comparison window:** ${period} before vs after each release\n`,
69
+ ];
70
+
71
+ for (const rel of releases) {
72
+ const { pre, post, goals } = rel.impact;
73
+ const windowHours = Math.round(rel.windowSeconds / 3600);
74
+
75
+ lines.push(`---\n### ${rel.category === 'feature' ? '✨' : rel.category === 'bugfix' ? '🐛' : rel.category === 'marketing' ? '📣' : '🚀'} ${rel.title}`);
76
+ lines.push(`**Released:** ${new Date(rel.releasedAt).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}`);
77
+ lines.push(`**Category:** ${rel.category} | **Comparison window:** ±${windowHours}h\n`);
78
+
79
+ if (rel.description) {
80
+ lines.push(`> ${rel.description}\n`);
81
+ }
82
+
83
+ if (rel.windowSeconds === 0) {
84
+ lines.push('_Release is too recent — impact data will be available shortly._\n');
85
+ continue;
86
+ }
87
+
88
+ lines.push('| Metric | Before | After | Change |');
89
+ lines.push('|--------|--------|-------|--------|');
90
+ lines.push(`| Page Views | ${pre.views.toLocaleString()} | ${post.views.toLocaleString()} | ${formatChange(pre.views, post.views)} |`);
91
+ lines.push(`| Unique Visitors | ${pre.visitors.toLocaleString()} | ${post.visitors.toLocaleString()} | ${formatChange(pre.visitors, post.visitors)} |`);
92
+
93
+ if (pre.revenue > 0 || post.revenue > 0) {
94
+ lines.push(`| Revenue | $${pre.revenue.toFixed(2)} | $${post.revenue.toFixed(2)} | ${formatChange(pre.revenue, post.revenue)} |`);
95
+ }
96
+
97
+ if (pre.errors > 0 || post.errors > 0) {
98
+ // For errors, an increase is bad (red), decrease is good (green)
99
+ const errChange = pct(pre.errors, post.errors);
100
+ const errStr = errChange === null ? '—'
101
+ : errChange > 0 ? `🔴 ▲ ${errChange}% more errors`
102
+ : errChange < 0 ? `🟢 ▼ ${Math.abs(errChange)}% fewer errors`
103
+ : '⚪ No change';
104
+ lines.push(`| Errors | ${pre.errors} | ${post.errors} | ${errStr} |`);
105
+ }
106
+
107
+ if (goals?.length > 0) {
108
+ lines.push('\n**Goal Conversions:**');
109
+ for (const g of goals) {
110
+ lines.push(`- **${g.name}**: ${g.preCompletions} → ${g.postCompletions} ${formatChange(g.preCompletions, g.postCompletions)}`);
111
+ }
112
+ }
113
+
114
+ lines.push('');
115
+ }
116
+
117
+ return { text: lines.join('\n') };
118
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * get-seo-suggestions tool
3
+ * Returns AI-generated SEO and UX improvement suggestions from Growtics.
4
+ */
5
+ export const definition = {
6
+ name: 'get-seo-suggestions',
7
+ description:
8
+ 'Get AI-generated SEO, UX, and performance improvement suggestions for your website. ' +
9
+ 'Each suggestion has an impact level (High/Medium/Low) and a status (Open/Solving/Solved). ' +
10
+ 'Use this to answer "What should I improve next for SEO?" or ' +
11
+ '"What are my most important open issues?"',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ siteId: {
16
+ type: 'string',
17
+ description: 'The Growtics site UUID (get it from list-sites)',
18
+ },
19
+ status: {
20
+ type: 'string',
21
+ enum: ['Open', 'Verifying', 'Solved', 'Failed'],
22
+ description: 'Filter by status. Leave empty to get all.',
23
+ },
24
+ category: {
25
+ type: 'string',
26
+ enum: ['SEO', 'UX', 'HTML', 'Errors', 'Journey'],
27
+ description: 'Filter by category. Leave empty to get all.',
28
+ },
29
+ },
30
+ required: ['siteId'],
31
+ },
32
+ };
33
+
34
+ const IMPACT_EMOJI = { High: '🔴', Medium: '🟡', Low: '🟢' };
35
+ const CATEGORY_EMOJI = { SEO: '🔍', UX: '🎨', HTML: '📄', Errors: '⚠️', Journey: '🗺️' };
36
+
37
+ export async function run({ args, apiClient }) {
38
+ const { siteId, status, category } = args;
39
+
40
+ const params = new URLSearchParams({ siteId });
41
+ if (status) params.set('status', status);
42
+ if (category) params.set('category', category);
43
+
44
+ const data = await apiClient.get(`/api/v1/suggestions?${params.toString()}`);
45
+
46
+ const { suggestions, summary } = data.data;
47
+ const meta = data.meta;
48
+
49
+ if (!suggestions?.length) {
50
+ const qualifier = status ? ` with status "${status}"` : '';
51
+ return {
52
+ text:
53
+ `## SEO & UX Suggestions — ${meta.siteName}\n\n` +
54
+ `No suggestions found${qualifier}. Growtics generates suggestions automatically ` +
55
+ `as it analyzes your site — check back soon!`,
56
+ };
57
+ }
58
+
59
+ const lines = [
60
+ `## SEO & UX Suggestions — ${meta.siteName}`,
61
+ `**Domain:** ${meta.domain} | **Total:** ${summary.total}\n`,
62
+ ];
63
+
64
+ // Summary by impact
65
+ if (summary.byStatus) {
66
+ const parts = Object.entries(summary.byStatus).map(([s, n]) => `${s}: ${n}`).join(' | ');
67
+ lines.push(`**By Status:** ${parts}`);
68
+ }
69
+ if (summary.byCategory) {
70
+ const parts = Object.entries(summary.byCategory).map(([c, n]) => `${CATEGORY_EMOJI[c] || '📌'} ${c}: ${n}`).join(' · ');
71
+ lines.push(`**By Category:** ${parts}\n`);
72
+ }
73
+
74
+ // Group by category for readability
75
+ const grouped = {};
76
+ for (const s of suggestions) {
77
+ if (!grouped[s.category]) grouped[s.category] = [];
78
+ grouped[s.category].push(s);
79
+ }
80
+
81
+ for (const [cat, items] of Object.entries(grouped)) {
82
+ lines.push(`### ${CATEGORY_EMOJI[cat] || '📌'} ${cat}`);
83
+ for (const s of items) {
84
+ const imp = IMPACT_EMOJI[s.impact] || '⚪';
85
+ const statusBadge = s.status === 'Solved' ? '✅' : s.status === 'Verifying' ? '🔄' : s.status === 'Failed' ? '❌' : '📋';
86
+ lines.push(`\n**${imp} [${s.impact}] ${s.title}** ${statusBadge} ${s.status}`);
87
+ lines.push(`> ${s.description}`);
88
+ }
89
+ lines.push('');
90
+ }
91
+
92
+ // Prioritized action list
93
+ const highImpactOpen = suggestions.filter(s => s.impact === 'High' && s.status === 'Open');
94
+ if (highImpactOpen.length > 0) {
95
+ lines.push('---');
96
+ lines.push('### 🎯 Recommended Next Actions (High Impact, Open)');
97
+ highImpactOpen.forEach((s, i) => {
98
+ lines.push(`${i + 1}. **${s.title}** (${s.category})`);
99
+ lines.push(` ${s.description}`);
100
+ });
101
+ }
102
+
103
+ return { text: lines.join('\n') };
104
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * get-top-pages tool
3
+ * Returns top pages by page views for a given period.
4
+ */
5
+ export const definition = {
6
+ name: 'get-top-pages',
7
+ description:
8
+ 'Get the top-performing pages on your website ranked by page views. ' +
9
+ 'Use this to answer "Which pages are getting the most traffic?" or ' +
10
+ '"Which pages are losing conversions?" when combined with goals data.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ siteId: {
15
+ type: 'string',
16
+ description: 'The Growtics site UUID (get it from list-sites)',
17
+ },
18
+ period: {
19
+ type: 'string',
20
+ enum: ['today', 'yesterday', '24h', '7d', '28d', '91d', 'mtd', 'lastmonth', 'ytd', '12m', 'all'],
21
+ description: 'Time period for the report. Default: 7d',
22
+ },
23
+ },
24
+ required: ['siteId'],
25
+ },
26
+ };
27
+
28
+ export async function run({ args, apiClient }) {
29
+ const { siteId, period = '7d' } = args;
30
+ const data = await apiClient.get(`/api/v1/analytics?siteId=${siteId}&period=${period}&type=overview`);
31
+
32
+ const { topPages } = data.data;
33
+ const meta = data.meta;
34
+
35
+ if (!topPages?.length) {
36
+ return { text: `No page data found for **${meta.siteName}** in the period \`${period}\`.` };
37
+ }
38
+
39
+ const total = topPages.reduce((s, p) => s + p.pageviews, 0);
40
+
41
+ const lines = [
42
+ `## Top Pages — ${meta.siteName}`,
43
+ `**Period:** ${period} | **Total across top pages:** ${total.toLocaleString()} views\n`,
44
+ `| # | Page | Views | Share |`,
45
+ `|---|------|-------|-------|`,
46
+ ...topPages.map((p, i) => {
47
+ const share = total > 0 ? ((p.pageviews / total) * 100).toFixed(1) : '0.0';
48
+ return `| ${i + 1} | \`${p.path}\` | ${p.pageviews.toLocaleString()} | ${share}% |`;
49
+ }),
50
+ ];
51
+
52
+ return { text: lines.join('\n') };
53
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * get-traffic-overview tool
3
+ * Returns the core traffic stats + time-series for a site.
4
+ */
5
+ export const definition = {
6
+ name: 'get-traffic-overview',
7
+ description:
8
+ 'Get a complete traffic overview for a Growtics site: total page views, unique visitors, ' +
9
+ 'active visitors right now, bounce rate, and a daily/hourly time-series breakdown. ' +
10
+ 'Use this to answer questions like "How is my traffic doing?" or "Why did traffic drop this week?"',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ siteId: {
15
+ type: 'string',
16
+ description: 'The Growtics site UUID (get it from list-sites)',
17
+ },
18
+ period: {
19
+ type: 'string',
20
+ enum: ['today', 'yesterday', '24h', '7d', '28d', '91d', 'mtd', 'lastmonth', 'ytd', '12m', 'all'],
21
+ description: 'Time period for the report. Default: 7d',
22
+ },
23
+ },
24
+ required: ['siteId'],
25
+ },
26
+ };
27
+
28
+ export async function run({ args, apiClient }) {
29
+ const { siteId, period = '7d' } = args;
30
+ const data = await apiClient.get(`/api/v1/analytics?siteId=${siteId}&period=${period}&type=overview`);
31
+
32
+ const { stats, timeSeries, topPages, topReferrers, topDevices, topCountries } = data.data;
33
+ const meta = data.meta;
34
+
35
+ // Trend: compare first half vs second half of time-series
36
+ let trendNote = '';
37
+ if (timeSeries.length >= 4) {
38
+ const mid = Math.floor(timeSeries.length / 2);
39
+ const first = timeSeries.slice(0, mid).reduce((s, d) => s + d.pageviews, 0);
40
+ const last = timeSeries.slice(mid).reduce((s, d) => s + d.pageviews, 0);
41
+ const pct = first > 0 ? Math.round(((last - first) / first) * 100) : 0;
42
+ if (pct > 5) trendNote = `📈 Traffic is **up ${pct}%** in the second half of this period.`;
43
+ else if (pct < -5) trendNote = `📉 Traffic is **down ${Math.abs(pct)}%** in the second half of this period.`;
44
+ else trendNote = `➡️ Traffic is **relatively stable** across this period.`;
45
+ }
46
+
47
+ const lines = [
48
+ `## Traffic Overview — ${meta.siteName} (${meta.domain})`,
49
+ `**Period:** ${period} | **Generated:** ${new Date(meta.generatedAt).toLocaleString()}\n`,
50
+
51
+ `### Core Metrics`,
52
+ `| Metric | Value |`,
53
+ `|--------|-------|`,
54
+ `| Page Views | **${stats.pageviews.toLocaleString()}** |`,
55
+ `| Unique Visitors | **${stats.uniqueVisitors.toLocaleString()}** |`,
56
+ `| Active Visitors (now) | **${stats.activeVisitors}** |`,
57
+ `| Bounce Rate | **${stats.bounceRate}%** |`,
58
+
59
+ trendNote ? `\n${trendNote}` : '',
60
+
61
+ `\n### Time Series (${timeSeries.length} data points)`,
62
+ timeSeries.length
63
+ ? timeSeries.map(d => `- ${d.time}: ${d.pageviews.toLocaleString()} views, ${d.uniqueVisitors.toLocaleString()} visitors`).join('\n')
64
+ : 'No data available.',
65
+
66
+ `\n### Top Pages`,
67
+ topPages.length
68
+ ? topPages.slice(0, 5).map((p, i) => `${i + 1}. \`${p.path}\` — ${p.pageviews.toLocaleString()} views`).join('\n')
69
+ : 'No page data.',
70
+
71
+ `\n### Top Referrers`,
72
+ topReferrers.length
73
+ ? topReferrers.slice(0, 5).map((r, i) => `${i + 1}. **${r.domain || '(direct)'}** — ${r.pageviews.toLocaleString()} views`).join('\n')
74
+ : 'No referrer data.',
75
+
76
+ `\n### Top Devices`,
77
+ topDevices.map(d => `- ${d.device}: ${d.pageviews.toLocaleString()} views`).join('\n') || 'No device data.',
78
+
79
+ `\n### Top Countries`,
80
+ topCountries.slice(0, 5).map(c => `- ${c.country}: ${c.pageviews.toLocaleString()} views`).join('\n') || 'No country data.',
81
+ ];
82
+
83
+ return { text: lines.filter(Boolean).join('\n') };
84
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * list-sites tool
3
+ * Lists all sites belonging to the authenticated Growtics account.
4
+ */
5
+ export const definition = {
6
+ name: 'list-sites',
7
+ description:
8
+ 'List all websites registered in your Growtics account. ' +
9
+ 'Returns each site\'s ID, name, domain, and creation date. ' +
10
+ 'Use this first to get siteId values needed by other tools.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {},
14
+ required: [],
15
+ },
16
+ };
17
+
18
+ export async function run({ apiClient }) {
19
+ const data = await apiClient.get('/api/v1/sites');
20
+
21
+ if (!data?.data?.length) {
22
+ return { text: 'No sites found in your Growtics account.' };
23
+ }
24
+
25
+ const lines = [
26
+ `## Your Growtics Sites (${data.meta.total} total)\n`,
27
+ ...data.data.map(
28
+ (s, i) =>
29
+ `**${i + 1}. ${s.name}**\n` +
30
+ ` - Site ID: \`${s.id}\`\n` +
31
+ ` - Domain: ${s.domain}\n` +
32
+ ` - Added: ${new Date(s.createdAt).toLocaleDateString()}`
33
+ ),
34
+ ];
35
+
36
+ return { text: lines.join('\n') };
37
+ }