dtc-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +71 -0
- package/dist/config.js.map +1 -0
- package/dist/cross-platform/correlator.d.ts +10 -0
- package/dist/cross-platform/correlator.js +166 -0
- package/dist/cross-platform/correlator.js.map +1 -0
- package/dist/cross-platform/tools.d.ts +2 -0
- package/dist/cross-platform/tools.js +30 -0
- package/dist/cross-platform/tools.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/platforms/klaviyo/client.d.ts +91 -0
- package/dist/platforms/klaviyo/client.js +389 -0
- package/dist/platforms/klaviyo/client.js.map +1 -0
- package/dist/platforms/klaviyo/tools.d.ts +2 -0
- package/dist/platforms/klaviyo/tools.js +368 -0
- package/dist/platforms/klaviyo/tools.js.map +1 -0
- package/dist/platforms/klaviyo/transforms.d.ts +59 -0
- package/dist/platforms/klaviyo/transforms.js +326 -0
- package/dist/platforms/klaviyo/transforms.js.map +1 -0
- package/dist/platforms/shopify/client.d.ts +51 -0
- package/dist/platforms/shopify/client.js +366 -0
- package/dist/platforms/shopify/client.js.map +1 -0
- package/dist/platforms/shopify/tools.d.ts +2 -0
- package/dist/platforms/shopify/tools.js +253 -0
- package/dist/platforms/shopify/tools.js.map +1 -0
- package/dist/platforms/shopify/transforms.d.ts +88 -0
- package/dist/platforms/shopify/transforms.js +191 -0
- package/dist/platforms/shopify/transforms.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +15 -0
- package/dist/server.js.map +1 -0
- package/dist/shared/cache.d.ts +23 -0
- package/dist/shared/cache.js +62 -0
- package/dist/shared/cache.js.map +1 -0
- package/dist/shared/errors.d.ts +25 -0
- package/dist/shared/errors.js +96 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/pagination.d.ts +21 -0
- package/dist/shared/pagination.js +36 -0
- package/dist/shared/pagination.js.map +1 -0
- package/dist/shared/types.d.ts +247 -0
- package/dist/shared/types.js +3 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rafael Sztutman
|
|
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,344 @@
|
|
|
1
|
+
# dtc-mcp
|
|
2
|
+
|
|
3
|
+
Context-optimized MCP server for DTC e-commerce brands. Connect Claude (or any MCP client) to your Klaviyo and Shopify data with 16 pre-built analytics tools.
|
|
4
|
+
|
|
5
|
+
Unlike raw API wrappers, dtc-mcp pre-aggregates data server-side and returns only actionable fields — using ~80% less context than dumping raw API responses into your conversation.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **8 Klaviyo tools** — campaign performance, flow breakdowns, subscriber health, profile search, event activity
|
|
10
|
+
- **7 Shopify tools** — sales summaries, daily/weekly/monthly trends, product performance, inventory alerts, customer cohorts, order search
|
|
11
|
+
- **2 cross-platform tools** — email revenue attribution, full DTC health dashboard
|
|
12
|
+
- **Dual revenue metrics** — both gross and net revenue on every sales query
|
|
13
|
+
- **Smart fallbacks** — ShopifyQL when available, GraphQL pagination when not
|
|
14
|
+
- **Aggressive caching** — respects Klaviyo's strict rate limits (1 req/s on reporting)
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g dtc-mcp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or run directly:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx dtc-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Setup with Claude Desktop
|
|
29
|
+
|
|
30
|
+
1. Open Claude Desktop
|
|
31
|
+
2. Go to **Settings** (gear icon) > **Developer** > **Edit Config**
|
|
32
|
+
3. Add the following to your `claude_desktop_config.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"dtc-mcp": {
|
|
38
|
+
"command": "npx",
|
|
39
|
+
"args": ["-y", "dtc-mcp"],
|
|
40
|
+
"env": {
|
|
41
|
+
"KLAVIYO_API_KEY": "pk_your_private_key_here",
|
|
42
|
+
"SHOPIFY_STORE": "your-store.myshopify.com",
|
|
43
|
+
"SHOPIFY_CLIENT_ID": "your_client_id",
|
|
44
|
+
"SHOPIFY_CLIENT_SECRET": "shpss_your_secret"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
4. Restart Claude Desktop
|
|
52
|
+
5. Look for the hammer icon in the chat input — that confirms the MCP tools are loaded
|
|
53
|
+
|
|
54
|
+
### Klaviyo-only mode
|
|
55
|
+
|
|
56
|
+
If you only use Klaviyo (no Shopify), just omit the Shopify variables. The 8 Klaviyo tools and subscriber analytics will work standalone. Shopify tools will return a helpful "not configured" message.
|
|
57
|
+
|
|
58
|
+
## Setup with ChatGPT
|
|
59
|
+
|
|
60
|
+
ChatGPT supports MCP servers via remote connections. Since dtc-mcp uses stdio transport (runs locally), you would need an MCP-to-HTTP bridge to expose it as a remote server. See [OpenAI's MCP documentation](https://platform.openai.com/docs/guides/tools-remote-mcp) for details on connecting remote MCP servers.
|
|
61
|
+
|
|
62
|
+
## Getting Your API Credentials
|
|
63
|
+
|
|
64
|
+
### Klaviyo API Key
|
|
65
|
+
|
|
66
|
+
1. Log into [Klaviyo](https://www.klaviyo.com/login)
|
|
67
|
+
2. Go to **Settings** (bottom-left) > **Account** > **Settings**
|
|
68
|
+
3. Click **API Keys** in the left sidebar
|
|
69
|
+
4. Click **Create Private API Key**
|
|
70
|
+
5. Give it a name (e.g., "dtc-mcp")
|
|
71
|
+
6. Select **Read-only** access for these scopes:
|
|
72
|
+
- `campaigns:read`
|
|
73
|
+
- `flows:read`
|
|
74
|
+
- `lists:read`
|
|
75
|
+
- `segments:read`
|
|
76
|
+
- `profiles:read`
|
|
77
|
+
- `metrics:read`
|
|
78
|
+
- `events:read`
|
|
79
|
+
7. Copy the key (starts with `pk_`)
|
|
80
|
+
|
|
81
|
+
### Shopify Credentials
|
|
82
|
+
|
|
83
|
+
There are two authentication methods. Use whichever matches your app type.
|
|
84
|
+
|
|
85
|
+
#### Option A: Dev Dashboard App (Recommended)
|
|
86
|
+
|
|
87
|
+
For apps created in the [Shopify Partners Dashboard](https://partners.shopify.com/) or Shopify CLI (required for new apps since January 2026):
|
|
88
|
+
|
|
89
|
+
1. Go to your app in the Partners Dashboard
|
|
90
|
+
2. Navigate to **Configuration** > **Client credentials**
|
|
91
|
+
3. Copy the **Client ID** and **Client Secret**
|
|
92
|
+
4. Your store URL is your `*.myshopify.com` domain
|
|
93
|
+
|
|
94
|
+
Set these environment variables:
|
|
95
|
+
```
|
|
96
|
+
SHOPIFY_STORE=your-store.myshopify.com
|
|
97
|
+
SHOPIFY_CLIENT_ID=your_client_id
|
|
98
|
+
SHOPIFY_CLIENT_SECRET=shpss_your_secret
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Required scopes:** `read_orders`, `read_products`, `read_customers`, `read_inventory`
|
|
102
|
+
|
|
103
|
+
#### Option B: Legacy Custom App
|
|
104
|
+
|
|
105
|
+
For custom apps created directly in Shopify Admin (apps created before January 2026):
|
|
106
|
+
|
|
107
|
+
1. Go to **Shopify Admin** > **Settings** > **Apps and sales channels**
|
|
108
|
+
2. Click **Develop apps** > select your app
|
|
109
|
+
3. Go to **API credentials** and copy the **Admin API access token**
|
|
110
|
+
|
|
111
|
+
Set these environment variables:
|
|
112
|
+
```
|
|
113
|
+
SHOPIFY_STORE=your-store.myshopify.com
|
|
114
|
+
SHOPIFY_ACCESS_TOKEN=shpat_your_token_here
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
> Do not set both `SHOPIFY_ACCESS_TOKEN` and `SHOPIFY_CLIENT_ID`/`SHOPIFY_CLIENT_SECRET` at the same time. The server will error if both are present.
|
|
118
|
+
|
|
119
|
+
## Environment Variables
|
|
120
|
+
|
|
121
|
+
| Variable | Required | Description |
|
|
122
|
+
|----------|----------|-------------|
|
|
123
|
+
| `KLAVIYO_API_KEY` | Yes | Klaviyo private API key (starts with `pk_`) |
|
|
124
|
+
| `SHOPIFY_STORE` | For Shopify | Your `*.myshopify.com` domain |
|
|
125
|
+
| `SHOPIFY_CLIENT_ID` | For Shopify (Dev Dashboard) | App client ID |
|
|
126
|
+
| `SHOPIFY_CLIENT_SECRET` | For Shopify (Dev Dashboard) | App client secret (starts with `shpss_`) |
|
|
127
|
+
| `SHOPIFY_ACCESS_TOKEN` | For Shopify (Legacy) | Admin API access token (starts with `shpat_`) |
|
|
128
|
+
| `SHOPIFY_API_VERSION` | No | Shopify API version (default: `2026-01`) |
|
|
129
|
+
| `KLAVIYO_CONVERSION_METRIC_ID` | No | Override auto-discovered "Placed Order" metric ID |
|
|
130
|
+
| `LOG_LEVEL` | No | `debug` \| `info` \| `warn` \| `error` (default: `info`) |
|
|
131
|
+
|
|
132
|
+
## Tool Reference
|
|
133
|
+
|
|
134
|
+
### Klaviyo Tools
|
|
135
|
+
|
|
136
|
+
#### `klaviyo_campaign_summary`
|
|
137
|
+
Top campaigns ranked by metric. Returns name, send date, opens, clicks, revenue.
|
|
138
|
+
|
|
139
|
+
| Parameter | Type | Default | Description |
|
|
140
|
+
|-----------|------|---------|-------------|
|
|
141
|
+
| `channel` | `"email"` \| `"sms"` | required | Channel filter |
|
|
142
|
+
| `metric` | `"revenue"` \| `"open_rate"` \| `"click_rate"` \| `"recipients"` | `"revenue"` | Rank by |
|
|
143
|
+
| `days` | 1-365 | 30 | Lookback period |
|
|
144
|
+
| `limit` | 1-25 | 10 | Max results |
|
|
145
|
+
|
|
146
|
+
> "Show me my top email campaigns by revenue this month"
|
|
147
|
+
|
|
148
|
+
#### `klaviyo_campaign_detail`
|
|
149
|
+
Deep dive on one campaign: full metrics, subject line, audiences, send time.
|
|
150
|
+
|
|
151
|
+
| Parameter | Type | Default | Description |
|
|
152
|
+
|-----------|------|---------|-------------|
|
|
153
|
+
| `campaign_id` | string | required | Klaviyo campaign ID |
|
|
154
|
+
|
|
155
|
+
> "Give me the full breakdown on my Black Friday campaign"
|
|
156
|
+
|
|
157
|
+
#### `klaviyo_flow_summary`
|
|
158
|
+
Top flows by metric. Returns name, status, trigger, message count, revenue.
|
|
159
|
+
|
|
160
|
+
| Parameter | Type | Default | Description |
|
|
161
|
+
|-----------|------|---------|-------------|
|
|
162
|
+
| `metric` | `"revenue"` \| `"click_rate"` \| `"conversion_rate"` \| `"recipients"` | `"revenue"` | Rank by |
|
|
163
|
+
| `days` | 1-365 | 30 | Lookback period |
|
|
164
|
+
| `status` | `"live"` \| `"draft"` \| `"manual"` \| `"all"` | `"live"` | Filter by status |
|
|
165
|
+
| `limit` | 1-25 | 10 | Max results |
|
|
166
|
+
|
|
167
|
+
> "Which of my flows generates the most revenue?"
|
|
168
|
+
|
|
169
|
+
#### `klaviyo_flow_detail`
|
|
170
|
+
Deep dive on one flow: per-message performance breakdown.
|
|
171
|
+
|
|
172
|
+
| Parameter | Type | Default | Description |
|
|
173
|
+
|-----------|------|---------|-------------|
|
|
174
|
+
| `flow_id` | string | required | Klaviyo flow ID |
|
|
175
|
+
| `days` | 1-365 | 30 | Lookback period |
|
|
176
|
+
|
|
177
|
+
> "Show me the per-email breakdown of my welcome flow"
|
|
178
|
+
|
|
179
|
+
#### `klaviyo_subscriber_health`
|
|
180
|
+
List growth and engagement tier breakdown.
|
|
181
|
+
|
|
182
|
+
| Parameter | Type | Default | Description |
|
|
183
|
+
|-----------|------|---------|-------------|
|
|
184
|
+
| `list_id` | string | optional | Specific list, or all lists |
|
|
185
|
+
|
|
186
|
+
> "What's the health of my email list?"
|
|
187
|
+
|
|
188
|
+
#### `klaviyo_list_segments`
|
|
189
|
+
All lists and segments with sizes.
|
|
190
|
+
|
|
191
|
+
| Parameter | Type | Default | Description |
|
|
192
|
+
|-----------|------|---------|-------------|
|
|
193
|
+
| `type` | `"lists"` \| `"segments"` \| `"all"` | `"all"` | Filter by type |
|
|
194
|
+
| `cursor` | string | optional | Pagination cursor |
|
|
195
|
+
|
|
196
|
+
> "List all my Klaviyo segments and their sizes"
|
|
197
|
+
|
|
198
|
+
#### `klaviyo_search_profiles`
|
|
199
|
+
Find profiles by email, phone, or name.
|
|
200
|
+
|
|
201
|
+
| Parameter | Type | Default | Description |
|
|
202
|
+
|-----------|------|---------|-------------|
|
|
203
|
+
| `query` | string | required | Email, phone, or name |
|
|
204
|
+
| `limit` | 1-10 | 5 | Max results |
|
|
205
|
+
|
|
206
|
+
> "Look up the profile for john@example.com"
|
|
207
|
+
|
|
208
|
+
#### `klaviyo_recent_activity`
|
|
209
|
+
Recent events for a metric (e.g., Placed Order, Opened Email).
|
|
210
|
+
|
|
211
|
+
| Parameter | Type | Default | Description |
|
|
212
|
+
|-----------|------|---------|-------------|
|
|
213
|
+
| `metric_name` | string | `"Placed Order"` | Metric name |
|
|
214
|
+
| `days` | 1-90 | 7 | Lookback period |
|
|
215
|
+
| `limit` | 1-25 | 10 | Max events |
|
|
216
|
+
| `profile_email` | string | optional | Filter to one profile |
|
|
217
|
+
|
|
218
|
+
> "Show me the last 10 orders placed"
|
|
219
|
+
|
|
220
|
+
### Shopify Tools
|
|
221
|
+
|
|
222
|
+
#### `shopify_sales_summary`
|
|
223
|
+
Revenue (gross + net), orders, AOV for a period with comparison.
|
|
224
|
+
|
|
225
|
+
| Parameter | Type | Default | Description |
|
|
226
|
+
|-----------|------|---------|-------------|
|
|
227
|
+
| `days` | 1-90 | 30 | Lookback period |
|
|
228
|
+
| `compare_previous` | boolean | true | Include previous period comparison |
|
|
229
|
+
|
|
230
|
+
> "What were my sales last month compared to the month before?"
|
|
231
|
+
|
|
232
|
+
#### `shopify_sales_timeseries`
|
|
233
|
+
Revenue and orders broken down by day, week, or month.
|
|
234
|
+
|
|
235
|
+
| Parameter | Type | Default | Description |
|
|
236
|
+
|-----------|------|---------|-------------|
|
|
237
|
+
| `days` | 1-365 | 30 | Lookback period |
|
|
238
|
+
| `granularity` | `"daily"` \| `"weekly"` \| `"monthly"` | `"daily"` | Bucket size |
|
|
239
|
+
|
|
240
|
+
> "Show me daily revenue for this month"
|
|
241
|
+
|
|
242
|
+
#### `shopify_product_performance`
|
|
243
|
+
Top products by revenue or units sold.
|
|
244
|
+
|
|
245
|
+
| Parameter | Type | Default | Description |
|
|
246
|
+
|-----------|------|---------|-------------|
|
|
247
|
+
| `days` | 1-90 | 7 | Lookback period |
|
|
248
|
+
| `metric` | `"revenue"` \| `"units"` | `"revenue"` | Rank by |
|
|
249
|
+
| `limit` | 1-25 | 10 | Max results |
|
|
250
|
+
|
|
251
|
+
> "Which products sold the most units this week?"
|
|
252
|
+
|
|
253
|
+
#### `shopify_order_search`
|
|
254
|
+
Find orders by number, email, or status.
|
|
255
|
+
|
|
256
|
+
| Parameter | Type | Default | Description |
|
|
257
|
+
|-----------|------|---------|-------------|
|
|
258
|
+
| `query` | string | required | Order number, email, or `financial_status:paid` |
|
|
259
|
+
| `limit` | 1-25 | 10 | Max results |
|
|
260
|
+
|
|
261
|
+
> "Find order #1234"
|
|
262
|
+
|
|
263
|
+
#### `shopify_inventory_alerts`
|
|
264
|
+
Products with low or zero stock, sorted by most urgent.
|
|
265
|
+
|
|
266
|
+
| Parameter | Type | Default | Description |
|
|
267
|
+
|-----------|------|---------|-------------|
|
|
268
|
+
| `threshold` | number | 10 | Alert at or below this quantity |
|
|
269
|
+
| `limit` | 1-50 | 20 | Max results |
|
|
270
|
+
|
|
271
|
+
> "Which products are running low on stock?"
|
|
272
|
+
|
|
273
|
+
#### `shopify_customer_cohort`
|
|
274
|
+
New vs returning buyers. First-time vs repeat split.
|
|
275
|
+
|
|
276
|
+
| Parameter | Type | Default | Description |
|
|
277
|
+
|-----------|------|---------|-------------|
|
|
278
|
+
| `days` | 1-365 | 90 | Lookback period |
|
|
279
|
+
| `limit` | 1-500 | 250 | Max customers to analyze |
|
|
280
|
+
|
|
281
|
+
> "What percentage of orders this quarter came from new customers?"
|
|
282
|
+
|
|
283
|
+
#### `shopify_recent_orders`
|
|
284
|
+
Most recent orders. Quick snapshot of store activity.
|
|
285
|
+
|
|
286
|
+
| Parameter | Type | Default | Description |
|
|
287
|
+
|-----------|------|---------|-------------|
|
|
288
|
+
| `limit` | 1-25 | 10 | Max results |
|
|
289
|
+
|
|
290
|
+
> "Show me the last 10 orders"
|
|
291
|
+
|
|
292
|
+
### Cross-Platform Tools
|
|
293
|
+
|
|
294
|
+
#### `dtc_email_revenue_attribution`
|
|
295
|
+
Email/SMS revenue vs total Shopify revenue. Shows email marketing contribution.
|
|
296
|
+
|
|
297
|
+
| Parameter | Type | Default | Description |
|
|
298
|
+
|-----------|------|---------|-------------|
|
|
299
|
+
| `days` | 1-365 | 30 | Lookback period |
|
|
300
|
+
|
|
301
|
+
> "What percentage of my revenue came from email?"
|
|
302
|
+
|
|
303
|
+
#### `dtc_dashboard`
|
|
304
|
+
Complete DTC health dashboard: sales + email + subscriber metrics in one call.
|
|
305
|
+
|
|
306
|
+
| Parameter | Type | Default | Description |
|
|
307
|
+
|-----------|------|---------|-------------|
|
|
308
|
+
| `days` | 7-90 | 30 | Lookback period |
|
|
309
|
+
|
|
310
|
+
> "Give me the full business dashboard for last month"
|
|
311
|
+
|
|
312
|
+
## Example Queries
|
|
313
|
+
|
|
314
|
+
Here are questions you can ask Claude once dtc-mcp is connected:
|
|
315
|
+
|
|
316
|
+
- "How did my email campaigns perform this month?"
|
|
317
|
+
- "Which flow is generating the most revenue? Drill into the top one."
|
|
318
|
+
- "Show me daily revenue for this month so I can compare against my Shopify dashboard"
|
|
319
|
+
- "What's my gross vs net revenue for the past 30 days?"
|
|
320
|
+
- "Which products are my best sellers this week?"
|
|
321
|
+
- "Are any products running low on stock?"
|
|
322
|
+
- "What percentage of my revenue comes from email marketing?"
|
|
323
|
+
- "How many new vs returning customers did I have this quarter?"
|
|
324
|
+
- "Look up the customer profile for sarah@example.com"
|
|
325
|
+
- "Give me a complete health dashboard for my business"
|
|
326
|
+
- "Compare this month's sales to last month"
|
|
327
|
+
- "What are my top SMS campaigns by click rate?"
|
|
328
|
+
|
|
329
|
+
## Development
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
git clone https://github.com/rafaelsztutman/dtc-mcp.git
|
|
333
|
+
cd dtc-mcp
|
|
334
|
+
npm install
|
|
335
|
+
cp .env.example .env # Fill in your API credentials
|
|
336
|
+
npm run build # Compile TypeScript
|
|
337
|
+
npm test # Run tests (31 tests)
|
|
338
|
+
npm run dev # Watch mode
|
|
339
|
+
npm run inspect # Open MCP Inspector for interactive testing
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## License
|
|
343
|
+
|
|
344
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
export type ShopifyAuthMode = "client_credentials" | "legacy" | null;
|
|
3
|
+
export interface Config {
|
|
4
|
+
readonly klaviyoApiKey: string;
|
|
5
|
+
readonly klaviyoRevision: string;
|
|
6
|
+
readonly shopifyStore: string | null;
|
|
7
|
+
readonly shopifyAccessToken: string | null;
|
|
8
|
+
readonly shopifyClientId: string | null;
|
|
9
|
+
readonly shopifyClientSecret: string | null;
|
|
10
|
+
readonly shopifyAuthMode: ShopifyAuthMode;
|
|
11
|
+
readonly shopifyApiVersion: string;
|
|
12
|
+
readonly klaviyoConversionMetricId: string | null;
|
|
13
|
+
readonly logLevel: LogLevel;
|
|
14
|
+
}
|
|
15
|
+
export declare const config: Config;
|
|
16
|
+
export declare function isShopifyConfigured(): boolean;
|
|
17
|
+
export declare function log(level: LogLevel, message: string, data?: Record<string, unknown>): void;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
function getRequired(name) {
|
|
2
|
+
const value = process.env[name];
|
|
3
|
+
if (!value) {
|
|
4
|
+
throw new Error(`Missing required environment variable: ${name}. ` +
|
|
5
|
+
`Set it in your environment: export ${name}=your_value_here`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function getOptional(name) {
|
|
10
|
+
return process.env[name] || null;
|
|
11
|
+
}
|
|
12
|
+
function loadConfig() {
|
|
13
|
+
const klaviyoApiKey = getRequired("KLAVIYO_API_KEY");
|
|
14
|
+
const shopifyStore = getOptional("SHOPIFY_STORE");
|
|
15
|
+
const shopifyAccessToken = getOptional("SHOPIFY_ACCESS_TOKEN");
|
|
16
|
+
const shopifyClientId = getOptional("SHOPIFY_CLIENT_ID");
|
|
17
|
+
const shopifyClientSecret = getOptional("SHOPIFY_CLIENT_SECRET");
|
|
18
|
+
// Determine auth mode
|
|
19
|
+
const hasLegacy = shopifyAccessToken !== null;
|
|
20
|
+
const hasClientCredentials = shopifyClientId !== null && shopifyClientSecret !== null;
|
|
21
|
+
if (hasLegacy && hasClientCredentials) {
|
|
22
|
+
throw new Error("Ambiguous Shopify config: both SHOPIFY_ACCESS_TOKEN and SHOPIFY_CLIENT_ID/SHOPIFY_CLIENT_SECRET are set. Use one auth mode, not both.");
|
|
23
|
+
}
|
|
24
|
+
let shopifyAuthMode = null;
|
|
25
|
+
if (hasClientCredentials) {
|
|
26
|
+
shopifyAuthMode = "client_credentials";
|
|
27
|
+
}
|
|
28
|
+
else if (hasLegacy) {
|
|
29
|
+
shopifyAuthMode = "legacy";
|
|
30
|
+
}
|
|
31
|
+
// Validate that store is set when any auth is configured
|
|
32
|
+
if (shopifyAuthMode && !shopifyStore) {
|
|
33
|
+
throw new Error("SHOPIFY_STORE is required when Shopify credentials are configured.");
|
|
34
|
+
}
|
|
35
|
+
// Validate partial client credentials
|
|
36
|
+
if ((shopifyClientId && !shopifyClientSecret) ||
|
|
37
|
+
(!shopifyClientId && shopifyClientSecret)) {
|
|
38
|
+
throw new Error("Both SHOPIFY_CLIENT_ID and SHOPIFY_CLIENT_SECRET must be set together.");
|
|
39
|
+
}
|
|
40
|
+
const logLevel = (process.env.LOG_LEVEL || "info");
|
|
41
|
+
if (!["debug", "info", "warn", "error"].includes(logLevel)) {
|
|
42
|
+
throw new Error(`Invalid LOG_LEVEL: ${logLevel}. Must be one of: debug, info, warn, error`);
|
|
43
|
+
}
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
klaviyoApiKey,
|
|
46
|
+
klaviyoRevision: "2026-01-15",
|
|
47
|
+
shopifyStore,
|
|
48
|
+
shopifyAccessToken,
|
|
49
|
+
shopifyClientId,
|
|
50
|
+
shopifyClientSecret,
|
|
51
|
+
shopifyAuthMode,
|
|
52
|
+
shopifyApiVersion: process.env.SHOPIFY_API_VERSION || "2026-01",
|
|
53
|
+
klaviyoConversionMetricId: getOptional("KLAVIYO_CONVERSION_METRIC_ID"),
|
|
54
|
+
logLevel,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export const config = loadConfig();
|
|
58
|
+
export function isShopifyConfigured() {
|
|
59
|
+
return config.shopifyStore !== null && config.shopifyAuthMode !== null;
|
|
60
|
+
}
|
|
61
|
+
export function log(level, message, data) {
|
|
62
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
63
|
+
if (levels.indexOf(level) < levels.indexOf(config.logLevel))
|
|
64
|
+
return;
|
|
65
|
+
const entry = data
|
|
66
|
+
? `[dtc-mcp] ${level}: ${message} ${JSON.stringify(data)}`
|
|
67
|
+
: `[dtc-mcp] ${level}: ${message}`;
|
|
68
|
+
// Always log to stderr — stdout is the MCP transport channel
|
|
69
|
+
console.error(entry);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAsBA,SAAS,WAAW,CAAC,IAAY;IAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,0CAA0C,IAAI,IAAI;YAChD,sCAAsC,IAAI,kBAAkB,CAC/D,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;AACnC,CAAC;AAED,SAAS,UAAU;IACjB,MAAM,aAAa,GAAG,WAAW,CAAC,iBAAiB,CAAC,CAAC;IAErD,MAAM,YAAY,GAAG,WAAW,CAAC,eAAe,CAAC,CAAC;IAClD,MAAM,kBAAkB,GAAG,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAC/D,MAAM,eAAe,GAAG,WAAW,CAAC,mBAAmB,CAAC,CAAC;IACzD,MAAM,mBAAmB,GAAG,WAAW,CAAC,uBAAuB,CAAC,CAAC;IAEjE,sBAAsB;IACtB,MAAM,SAAS,GAAG,kBAAkB,KAAK,IAAI,CAAC;IAC9C,MAAM,oBAAoB,GACxB,eAAe,KAAK,IAAI,IAAI,mBAAmB,KAAK,IAAI,CAAC;IAE3D,IAAI,SAAS,IAAI,oBAAoB,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CACb,uIAAuI,CACxI,CAAC;IACJ,CAAC;IAED,IAAI,eAAe,GAAoB,IAAI,CAAC;IAC5C,IAAI,oBAAoB,EAAE,CAAC;QACzB,eAAe,GAAG,oBAAoB,CAAC;IACzC,CAAC;SAAM,IAAI,SAAS,EAAE,CAAC;QACrB,eAAe,GAAG,QAAQ,CAAC;IAC7B,CAAC;IAED,yDAAyD;IACzD,IAAI,eAAe,IAAI,CAAC,YAAY,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,oEAAoE,CACrE,CAAC;IACJ,CAAC;IAED,sCAAsC;IACtC,IACE,CAAC,eAAe,IAAI,CAAC,mBAAmB,CAAC;QACzC,CAAC,CAAC,eAAe,IAAI,mBAAmB,CAAC,EACzC,CAAC;QACD,MAAM,IAAI,KAAK,CACb,wEAAwE,CACzE,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM,CAAa,CAAC;IAC/D,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CACb,sBAAsB,QAAQ,4CAA4C,CAC3E,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,aAAa;QACb,eAAe,EAAE,YAAY;QAC7B,YAAY;QACZ,kBAAkB;QAClB,eAAe;QACf,mBAAmB;QACnB,eAAe;QACf,iBAAiB,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,SAAS;QAC/D,yBAAyB,EAAE,WAAW,CAAC,8BAA8B,CAAC;QACtE,QAAQ;KACT,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;AAEnC,MAAM,UAAU,mBAAmB;IACjC,OAAO,MAAM,CAAC,YAAY,KAAK,IAAI,IAAI,MAAM,CAAC,eAAe,KAAK,IAAI,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,GAAG,CACjB,KAAe,EACf,OAAe,EACf,IAA8B;IAE9B,MAAM,MAAM,GAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9D,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC;QAAE,OAAO;IAEpE,MAAM,KAAK,GAAG,IAAI;QAChB,CAAC,CAAC,aAAa,KAAK,KAAK,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;QAC1D,CAAC,CAAC,aAAa,KAAK,KAAK,OAAO,EAAE,CAAC;IAErC,6DAA6D;IAC7D,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RevenueAttribution, Dashboard } from "../shared/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compare Klaviyo attributed revenue vs Shopify total revenue.
|
|
4
|
+
*/
|
|
5
|
+
export declare function computeRevenueAttribution(days: number): Promise<RevenueAttribution>;
|
|
6
|
+
/**
|
|
7
|
+
* Orchestrate a complete DTC health dashboard.
|
|
8
|
+
* Uses cached results from sub-queries to minimize API calls.
|
|
9
|
+
*/
|
|
10
|
+
export declare function computeDashboard(days: number): Promise<Dashboard>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { isShopifyConfigured } from "../config.js";
|
|
2
|
+
import { getCampaignReport, getFlowReport } from "../platforms/klaviyo/client.js";
|
|
3
|
+
import { getAggregatedSales } from "../platforms/shopify/client.js";
|
|
4
|
+
/**
|
|
5
|
+
* Compare Klaviyo attributed revenue vs Shopify total revenue.
|
|
6
|
+
*/
|
|
7
|
+
export async function computeRevenueAttribution(days) {
|
|
8
|
+
// Fetch Klaviyo campaign + flow revenue (uses cached reporting data)
|
|
9
|
+
const [campaignRows, flowRows] = await Promise.all([
|
|
10
|
+
getCampaignReport(days),
|
|
11
|
+
getFlowReport(days),
|
|
12
|
+
]);
|
|
13
|
+
// Sum campaign revenue
|
|
14
|
+
let campaignRevenue = 0;
|
|
15
|
+
const campaignRevenueMap = new Map();
|
|
16
|
+
for (const row of campaignRows) {
|
|
17
|
+
const revenue = row.statistics.conversion_value ?? 0;
|
|
18
|
+
const id = String(row.groupings.campaign_id ?? "unknown");
|
|
19
|
+
campaignRevenue += revenue;
|
|
20
|
+
const existing = campaignRevenueMap.get(id);
|
|
21
|
+
if (existing) {
|
|
22
|
+
existing.revenue += revenue;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
campaignRevenueMap.set(id, { name: id, revenue });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Sum flow revenue
|
|
29
|
+
let flowRevenue = 0;
|
|
30
|
+
const flowRevenueMap = new Map();
|
|
31
|
+
for (const row of flowRows) {
|
|
32
|
+
const revenue = row.statistics.conversion_value ?? 0;
|
|
33
|
+
const id = String(row.groupings.flow_id ?? "unknown");
|
|
34
|
+
flowRevenue += revenue;
|
|
35
|
+
const existing = flowRevenueMap.get(id);
|
|
36
|
+
if (existing) {
|
|
37
|
+
existing.revenue += revenue;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
flowRevenueMap.set(id, { name: id, revenue });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const emailTotalRevenue = campaignRevenue + flowRevenue;
|
|
44
|
+
// Get Shopify total revenue
|
|
45
|
+
let totalRevenue = emailTotalRevenue;
|
|
46
|
+
if (isShopifyConfigured()) {
|
|
47
|
+
try {
|
|
48
|
+
const sales = await getAggregatedSales(days);
|
|
49
|
+
totalRevenue = sales.netRevenue || emailTotalRevenue;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Shopify unavailable — use email revenue as total
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const emailPct = totalRevenue > 0
|
|
56
|
+
? Math.round((emailTotalRevenue / totalRevenue) * 10000) / 100
|
|
57
|
+
: 0;
|
|
58
|
+
const topCampaigns = Array.from(campaignRevenueMap.values())
|
|
59
|
+
.sort((a, b) => b.revenue - a.revenue)
|
|
60
|
+
.slice(0, 5);
|
|
61
|
+
const topFlows = Array.from(flowRevenueMap.values())
|
|
62
|
+
.sort((a, b) => b.revenue - a.revenue)
|
|
63
|
+
.slice(0, 5);
|
|
64
|
+
return {
|
|
65
|
+
period_days: days,
|
|
66
|
+
total_revenue: Math.round(totalRevenue * 100) / 100,
|
|
67
|
+
email_campaign_revenue: Math.round(campaignRevenue * 100) / 100,
|
|
68
|
+
flow_revenue: Math.round(flowRevenue * 100) / 100,
|
|
69
|
+
email_total_revenue: Math.round(emailTotalRevenue * 100) / 100,
|
|
70
|
+
email_pct_of_total: emailPct,
|
|
71
|
+
flow_vs_campaign_split: {
|
|
72
|
+
campaign_pct: emailTotalRevenue > 0
|
|
73
|
+
? Math.round((campaignRevenue / emailTotalRevenue) * 10000) / 100
|
|
74
|
+
: 0,
|
|
75
|
+
flow_pct: emailTotalRevenue > 0
|
|
76
|
+
? Math.round((flowRevenue / emailTotalRevenue) * 10000) / 100
|
|
77
|
+
: 0,
|
|
78
|
+
},
|
|
79
|
+
top_revenue_campaigns: topCampaigns,
|
|
80
|
+
top_revenue_flows: topFlows,
|
|
81
|
+
note: "Revenue attribution uses Klaviyo's send-date model. Actual overlap with Shopify revenue may vary.",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Orchestrate a complete DTC health dashboard.
|
|
86
|
+
* Uses cached results from sub-queries to minimize API calls.
|
|
87
|
+
*/
|
|
88
|
+
export async function computeDashboard(days) {
|
|
89
|
+
// Run Klaviyo queries in parallel (these hit cache if recent calls were made)
|
|
90
|
+
const [campaignRows, flowRows] = await Promise.all([
|
|
91
|
+
getCampaignReport(days),
|
|
92
|
+
getFlowReport(days),
|
|
93
|
+
]);
|
|
94
|
+
// Sum email revenue
|
|
95
|
+
let campaignRevenue = 0;
|
|
96
|
+
const topCampaigns = [];
|
|
97
|
+
const campaignMap = new Map();
|
|
98
|
+
for (const row of campaignRows) {
|
|
99
|
+
const id = String(row.groupings.campaign_id ?? "");
|
|
100
|
+
const existing = campaignMap.get(id) ?? { revenue: 0, opens: 0, recipients: 0 };
|
|
101
|
+
existing.revenue += row.statistics.conversion_value ?? 0;
|
|
102
|
+
existing.opens += row.statistics.opens_unique ?? row.statistics.opens ?? 0;
|
|
103
|
+
existing.recipients += row.statistics.recipients ?? 0;
|
|
104
|
+
campaignMap.set(id, existing);
|
|
105
|
+
campaignRevenue += row.statistics.conversion_value ?? 0;
|
|
106
|
+
}
|
|
107
|
+
for (const [name, stats] of campaignMap.entries()) {
|
|
108
|
+
topCampaigns.push({
|
|
109
|
+
name,
|
|
110
|
+
revenue: Math.round(stats.revenue * 100) / 100,
|
|
111
|
+
open_rate: stats.recipients > 0
|
|
112
|
+
? Math.round((stats.opens / stats.recipients) * 10000) / 10000
|
|
113
|
+
: 0,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
topCampaigns.sort((a, b) => b.revenue - a.revenue);
|
|
117
|
+
let flowRevenue = 0;
|
|
118
|
+
const topFlows = [];
|
|
119
|
+
const flowMap = new Map();
|
|
120
|
+
for (const row of flowRows) {
|
|
121
|
+
const id = String(row.groupings.flow_id ?? "");
|
|
122
|
+
flowMap.set(id, (flowMap.get(id) ?? 0) + (row.statistics.conversion_value ?? 0));
|
|
123
|
+
flowRevenue += row.statistics.conversion_value ?? 0;
|
|
124
|
+
}
|
|
125
|
+
for (const [name, revenue] of flowMap.entries()) {
|
|
126
|
+
topFlows.push({ name, revenue: Math.round(revenue * 100) / 100 });
|
|
127
|
+
}
|
|
128
|
+
topFlows.sort((a, b) => b.revenue - a.revenue);
|
|
129
|
+
const emailRevenue = campaignRevenue + flowRevenue;
|
|
130
|
+
// Shopify sales
|
|
131
|
+
let shopifyRevenue = emailRevenue;
|
|
132
|
+
let orderCount = 0;
|
|
133
|
+
let aov = 0;
|
|
134
|
+
if (isShopifyConfigured()) {
|
|
135
|
+
try {
|
|
136
|
+
const sales = await getAggregatedSales(days);
|
|
137
|
+
shopifyRevenue = sales.netRevenue || emailRevenue;
|
|
138
|
+
orderCount = sales.orderCount;
|
|
139
|
+
aov = orderCount > 0 ? Math.round((shopifyRevenue / orderCount) * 100) / 100 : 0;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Shopify unavailable
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
period_days: days,
|
|
147
|
+
sales: {
|
|
148
|
+
revenue: Math.round(shopifyRevenue * 100) / 100,
|
|
149
|
+
orders: orderCount,
|
|
150
|
+
aov,
|
|
151
|
+
},
|
|
152
|
+
email: {
|
|
153
|
+
email_revenue: Math.round(emailRevenue * 100) / 100,
|
|
154
|
+
email_pct_of_total: shopifyRevenue > 0
|
|
155
|
+
? Math.round((emailRevenue / shopifyRevenue) * 10000) / 100
|
|
156
|
+
: 0,
|
|
157
|
+
top_campaigns: topCampaigns.slice(0, 5),
|
|
158
|
+
top_flows: topFlows.slice(0, 5),
|
|
159
|
+
},
|
|
160
|
+
subscribers: {
|
|
161
|
+
total: 0, // Would require a separate API call — omit to save rate limits
|
|
162
|
+
list_count: 0,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=correlator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlator.js","sourceRoot":"","sources":["../../src/cross-platform/correlator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAClF,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAGpE;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,IAAY;IAEZ,qEAAqE;IACrE,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACjD,iBAAiB,CAAC,IAAI,CAAC;QACvB,aAAa,CAAC,IAAI,CAAC;KACpB,CAAC,CAAC;IAEH,uBAAuB;IACvB,IAAI,eAAe,GAAG,CAAC,CAAC;IACxB,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAA6C,CAAC;IAChF,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,CAAC,gBAAgB,IAAI,CAAC,CAAC;QACrD,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,WAAW,IAAI,SAAS,CAAC,CAAC;QAC1D,eAAe,IAAI,OAAO,CAAC;QAC3B,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5C,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC;QAC9B,CAAC;aAAM,CAAC;YACN,kBAAkB,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IAED,mBAAmB;IACnB,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,MAAM,cAAc,GAAG,IAAI,GAAG,EAA6C,CAAC;IAC5E,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,CAAC,gBAAgB,IAAI,CAAC,CAAC;QACrD,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,IAAI,SAAS,CAAC,CAAC;QACtD,WAAW,IAAI,OAAO,CAAC;QACvB,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACxC,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC;QAC9B,CAAC;aAAM,CAAC;YACN,cAAc,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAED,MAAM,iBAAiB,GAAG,eAAe,GAAG,WAAW,CAAC;IAExD,4BAA4B;IAC5B,IAAI,YAAY,GAAG,iBAAiB,CAAC;IACrC,IAAI,mBAAmB,EAAE,EAAE,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,kBAAkB,CAAC,IAAI,CAAC,CAAC;YAC7C,YAAY,GAAG,KAAK,CAAC,UAAU,IAAI,iBAAiB,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,mDAAmD;QACrD,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,YAAY,GAAG,CAAC;QAC/B,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,iBAAiB,GAAG,YAAY,CAAC,GAAG,KAAK,CAAC,GAAG,GAAG;QAC9D,CAAC,CAAC,CAAC,CAAC;IAEN,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,CAAC;SACzD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;SACrC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAEf,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;SACjD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;SACrC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAEf,OAAO;QACL,WAAW,EAAE,IAAI;QACjB,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,GAAG,CAAC,GAAG,GAAG;QACnD,sBAAsB,EAAE,IAAI,CAAC,KAAK,CAAC,eAAe,GAAG,GAAG,CAAC,GAAG,GAAG;QAC/D,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,GAAG,CAAC,GAAG,GAAG;QACjD,mBAAmB,EAAE,IAAI,CAAC,KAAK,CAAC,iBAAiB,GAAG,GAAG,CAAC,GAAG,GAAG;QAC9D,kBAAkB,EAAE,QAAQ;QAC5B,sBAAsB,EAAE;YACtB,YAAY,EACV,iBAAiB,GAAG,CAAC;gBACnB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,eAAe,GAAG,iBAAiB,CAAC,GAAG,KAAK,CAAC,GAAG,GAAG;gBACjE,CAAC,CAAC,CAAC;YACP,QAAQ,EACN,iBAAiB,GAAG,CAAC;gBACnB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,GAAG,iBAAiB,CAAC,GAAG,KAAK,CAAC,GAAG,GAAG;gBAC7D,CAAC,CAAC,CAAC;SACR;QACD,qBAAqB,EAAE,YAAY;QACnC,iBAAiB,EAAE,QAAQ;QAC3B,IAAI,EAAE,mGAAmG;KAC1G,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY;IACjD,8EAA8E;IAC9E,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACjD,iBAAiB,CAAC,IAAI,CAAC;QACvB,aAAa,CAAC,IAAI,CAAC;KACpB,CAAC,CAAC;IAEH,oBAAoB;IACpB,IAAI,eAAe,GAAG,CAAC,CAAC;IACxB,MAAM,YAAY,GAAgE,EAAE,CAAC;IACrF,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkE,CAAC;IAE9F,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;QACnD,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;QAChF,QAAQ,CAAC,OAAO,IAAI,GAAG,CAAC,UAAU,CAAC,gBAAgB,IAAI,CAAC,CAAC;QACzD,QAAQ,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC,CAAC;QAC3E,QAAQ,CAAC,UAAU,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,IAAI,CAAC,CAAC;QACtD,WAAW,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QAC9B,eAAe,IAAI,GAAG,CAAC,UAAU,CAAC,gBAAgB,IAAI,CAAC,CAAC;IAC1D,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,CAAC;QAClD,YAAY,CAAC,IAAI,CAAC;YAChB,IAAI;YACJ,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,GAAG;YAC9C,SAAS,EAAE,KAAK,CAAC,UAAU,GAAG,CAAC;gBAC7B,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC,GAAG,KAAK;gBAC9D,CAAC,CAAC,CAAC;SACN,CAAC,CAAC;IACL,CAAC;IACD,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;IAEnD,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,MAAM,QAAQ,GAA6C,EAAE,CAAC;IAC9D,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE1C,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;QAC/C,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,gBAAgB,IAAI,CAAC,CAAC,CAAC,CAAC;QACjF,WAAW,IAAI,GAAG,CAAC,UAAU,CAAC,gBAAgB,IAAI,CAAC,CAAC;IACtD,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QAChD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC;IACpE,CAAC;IACD,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;IAE/C,MAAM,YAAY,GAAG,eAAe,GAAG,WAAW,CAAC;IAEnD,gBAAgB;IAChB,IAAI,cAAc,GAAG,YAAY,CAAC;IAClC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,GAAG,GAAG,CAAC,CAAC;IAEZ,IAAI,mBAAmB,EAAE,EAAE,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,kBAAkB,CAAC,IAAI,CAAC,CAAC;YAC7C,cAAc,GAAG,KAAK,CAAC,UAAU,IAAI,YAAY,CAAC;YAClD,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC;YAC9B,GAAG,GAAG,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,cAAc,GAAG,UAAU,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACnF,CAAC;QAAC,MAAM,CAAC;YACP,sBAAsB;QACxB,CAAC;IACH,CAAC;IAED,OAAO;QACL,WAAW,EAAE,IAAI;QACjB,KAAK,EAAE;YACL,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,GAAG,CAAC,GAAG,GAAG;YAC/C,MAAM,EAAE,UAAU;YAClB,GAAG;SACJ;QACD,KAAK,EAAE;YACL,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,GAAG,CAAC,GAAG,GAAG;YACnD,kBAAkB,EAChB,cAAc,GAAG,CAAC;gBAChB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,YAAY,GAAG,cAAc,CAAC,GAAG,KAAK,CAAC,GAAG,GAAG;gBAC3D,CAAC,CAAC,CAAC;YACP,aAAa,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;YACvC,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;SAChC;QACD,WAAW,EAAE;YACX,KAAK,EAAE,CAAC,EAAE,+DAA+D;YACzE,UAAU,EAAE,CAAC;SACd;KACF,CAAC;AACJ,CAAC"}
|