dtc-mcp 1.0.3 → 1.0.5

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/data/docs.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "v2026-05-25",
3
- "generatedAt": "2026-05-25T02:57:04.731Z",
3
+ "generatedAt": "2026-05-25T22:02:22.894Z",
4
4
  "chunks": [
5
5
  {
6
6
  "id": "guide.output-discipline",
@@ -52,8 +52,8 @@
52
52
  "title": "Recipe: top N campaigns by revenue (replaces klaviyo_campaign_summary)",
53
53
  "platform": "guide",
54
54
  "category": "recipe",
55
- "summary": "Replicates the old klaviyo_campaign_summary tool in <20 lines.",
56
- "content": "```js\nconst metricId = await klaviyo.getConversionMetricId();\nconst report = await klaviyo.reporting.campaignValues({\n data: { type: 'campaign-values-report', attributes: {\n timeframe: { key: 'last_30_days' },\n conversion_metric_id: metricId,\n statistics: ['recipients', 'opens_unique', 'open_rate', 'clicks_unique', 'click_rate', 'conversion_value'],\n }}\n});\n\nconst rows = (report.data.attributes.results || [])\n .map(r => ({\n campaign_id: r.groupings.campaign_id,\n recipients: r.statistics.recipients,\n open_rate: r.statistics.open_rate,\n click_rate: r.statistics.click_rate,\n revenue: r.statistics.conversion_value,\n }))\n .sort((a, b) => b.revenue - a.revenue)\n .slice(0, 10);\n\n// Hydrate names (only for the top 10 — saves rate-limit budget)\nfor (const row of rows) {\n const { data } = await klaviyo.campaigns.get(row.campaign_id, {\n 'fields[campaign]': 'name,send_time',\n });\n row.name = data.attributes.name;\n row.send_time = data.attributes.send_time;\n}\nreturn rows;\n```",
55
+ "summary": "Replicates the old klaviyo_campaign_summary tool in <30 lines.",
56
+ "content": "Aggregates report rows up to the campaign level before ranking — see `klaviyo.reporting.campaignValues` for why the raw rows are per-message and must be summed first.\n\n```js\nconst metricId = await klaviyo.getConversionMetricId();\nconst report = await klaviyo.reporting.campaignValues({\n data: { type: 'campaign-values-report', attributes: {\n timeframe: { key: 'last_30_days' },\n conversion_metric_id: metricId,\n statistics: ['recipients', 'opens_unique', 'clicks_unique', 'conversion_value'],\n }}\n});\n\n// Sum across all (campaign_id, message_id, channel) rows for each campaign_id.\nconst byCampaign = new Map();\nfor (const r of report.data.attributes.results || []) {\n const id = r.groupings.campaign_id;\n const slot = byCampaign.get(id) ?? { campaign_id: id, recipients: 0, opens_unique: 0, clicks_unique: 0, revenue: 0 };\n slot.recipients += r.statistics.recipients || 0;\n slot.opens_unique += r.statistics.opens_unique || 0;\n slot.clicks_unique += r.statistics.clicks_unique || 0;\n slot.revenue += r.statistics.conversion_value || 0;\n byCampaign.set(id, slot);\n}\n\nconst rows = [...byCampaign.values()]\n .map(c => ({ ...c,\n open_rate: c.recipients ? c.opens_unique / c.recipients : 0,\n click_rate: c.recipients ? c.clicks_unique / c.recipients : 0,\n }))\n .sort((a, b) => b.revenue - a.revenue)\n .slice(0, 10);\n\n// Hydrate names (only for the top 10 — saves rate-limit budget)\nfor (const row of rows) {\n const { data } = await klaviyo.campaigns.get(row.campaign_id, {\n 'fields[campaign]': 'name,send_time',\n });\n row.name = data.attributes.name;\n row.send_time = data.attributes.send_time;\n}\nreturn rows;\n```",
57
57
  "tags": [
58
58
  "recipe",
59
59
  "campaigns",
@@ -61,6 +61,21 @@
61
61
  "top"
62
62
  ]
63
63
  },
64
+ {
65
+ "id": "guide.recipe.top-flows",
66
+ "title": "Recipe: top N flows by revenue",
67
+ "platform": "guide",
68
+ "category": "recipe",
69
+ "summary": "Rank live flows by 30-day attributed revenue. Aggregates per-message report rows up to flow level.",
70
+ "content": "Flow value reports return one row per (flow_id, flow_message_id, send_channel). A 5-message welcome flow shows up as 5 rows. To rank FLOWS (not individual messages within flows) you have to sum the per-message rows. Then optionally hydrate names + filter to live status.\n\n```js\nconst metricId = await klaviyo.getConversionMetricId();\nconst report = await klaviyo.reporting.flowValues({\n data: { type: 'flow-values-report', attributes: {\n timeframe: { key: 'last_30_days' },\n conversion_metric_id: metricId,\n statistics: ['recipients', 'opens_unique', 'clicks_unique', 'conversion_value'],\n }}\n});\n\n// Sum across all rows for each flow_id.\nconst byFlow = new Map();\nfor (const r of report.data.attributes.results || []) {\n const id = r.groupings.flow_id;\n const slot = byFlow.get(id) ?? { flow_id: id, recipients: 0, opens_unique: 0, clicks_unique: 0, revenue: 0, messageCount: 0 };\n slot.recipients += r.statistics.recipients || 0;\n slot.opens_unique += r.statistics.opens_unique || 0;\n slot.clicks_unique += r.statistics.clicks_unique || 0;\n slot.revenue += r.statistics.conversion_value || 0;\n slot.messageCount += 1;\n byFlow.set(id, slot);\n}\n\nconst rows = [...byFlow.values()]\n .map(f => ({ ...f,\n open_rate: f.recipients ? f.opens_unique / f.recipients : 0,\n click_rate: f.recipients ? f.clicks_unique / f.recipients : 0,\n }))\n .sort((a, b) => b.revenue - a.revenue)\n .slice(0, 5);\n\n// Hydrate names + status + trigger_type. Filter to live if the user asked for it.\nfor (const row of rows) {\n const { data } = await klaviyo.flows.get(row.flow_id, {\n 'fields[flow]': 'name,status,trigger_type',\n });\n row.name = data.attributes.name;\n row.status = data.attributes.status;\n row.trigger_type = data.attributes.trigger_type;\n}\nreturn rows;\n```",
71
+ "tags": [
72
+ "recipe",
73
+ "flows",
74
+ "summary",
75
+ "top",
76
+ "revenue"
77
+ ]
78
+ },
64
79
  {
65
80
  "id": "guide.shopify-not-configured",
66
81
  "title": "Klaviyo-only mode (no Shopify)",
@@ -80,7 +95,7 @@
80
95
  "platform": "guide",
81
96
  "category": "guide",
82
97
  "summary": "The sandbox keeps one context alive per MCP connection. Assign to globalThis to share data across execute_code calls. Beats Stainless's stateless Cloudflare-Workers model for iterative DTC analyses.",
83
- "content": "## Stateful sandbox sessions\n\nThe sandbox keeps a **single context alive for the lifetime of your MCP connection**. Variables you assign to `globalThis` in one `execute_code` call are visible in every subsequent call within the same conversation. This is intentional and is one of the architectural differences from Stainless (which runs each call in a fresh Cloudflare Worker isolate — stateless).\n\nFor multi-step DTC analyses (a typical workflow: fetch campaigns → drill into a specific one → cross-reference Shopify orders), this means:\n- The first call fetches and caches expensive data into `globalThis`.\n- Subsequent calls reference those values directly without re-fetching.\n- The host's reporting cache STILL handles repeat API calls within the same session — statefulness compounds with it.\n\n### Sharing data: use globalThis\n\nBecause user code runs as a fresh script per call, top-level `const` / `let` declarations are **scoped to that call only** — they're NOT visible later. Use `globalThis` (or `globalThis.x = ...` shorthand) for anything you want to carry forward.\n\n```js\n// Call 1\nconst metricId = await klaviyo.getConversionMetricId();\nglobalThis.metricId = metricId; // persisted\nconst topCampaigns = topN(\n (await klaviyo.reporting.campaignValues({\n data: { type: 'campaign-values-report', attributes: {\n timeframe: { key: 'last_30_days' },\n conversion_metric_id: metricId,\n statistics: ['conversion_value', 'recipients']\n }}\n })).data.attributes.results,\n 5,\n (r) => r.statistics.conversion_value\n);\nglobalThis.topCampaigns = topCampaigns; // persisted\nreturn pick(topCampaigns, { groupings: { campaign_id: true }, statistics: { conversion_value: true } });\n```\n\n```js\n// Call 2 — no re-fetch needed\nconst topIds = globalThis.topCampaigns.map(c => c.groupings.campaign_id);\nconst details = await Promise.all(topIds.map(id =>\n klaviyo.campaigns.get(id, { 'fields[campaign]': 'name,send_time' })\n));\nreturn details.map(d => ({ id: d.data.id, name: d.data.attributes.name }));\n```\n\n### Session reset (TTL + memory)\n\nThe context is recreated when any of the following happens:\n- The MCP connection closes (Claude Desktop is quit or the extension is disabled)\n- 30 minutes of idle time pass with no `execute_code` call (configurable via env)\n- Memory usage exceeds the per-isolate cap (256 MB)\n- The user manually reloads the extension\n\nWhen the context is recreated, the next call's response includes `sessionReset: true` and a `sessionResetNote`. If you see that, any globals you set previously are gone — re-declare what you need.\n\n```js\n// You can detect a reset by checking globalThis.\nif (typeof globalThis.cachedMetricId === 'undefined') {\n globalThis.cachedMetricId = await klaviyo.getConversionMetricId();\n}\nconst metricId = globalThis.cachedMetricId;\n```\n\n### Why this beats Stainless's stateless model\n\nStainless's Cloudflare-Workers sandbox runs each `execute` in a fresh isolate. For iterative analyses they require re-fetching at every step, which inflates token cost AND duration. Our self-hosted sidecar trivially supports state because it owns the isolate lifecycle.",
98
+ "content": "## Stateful sandbox sessions\n\nThe sandbox keeps a **single context alive for the lifetime of your MCP connection**. Variables you assign to `globalThis` in one `execute_code` call are visible in every subsequent call within the same conversation. This is intentional and is one of the architectural differences from Stainless (which runs each call in a fresh Cloudflare Worker isolate — stateless).\n\nFor multi-step DTC analyses (a typical workflow: fetch campaigns → drill into a specific one → cross-reference Shopify orders), this means:\n- The first call fetches and caches expensive data into `globalThis`.\n- Subsequent calls reference those values directly without re-fetching.\n- The host's reporting cache STILL handles repeat API calls within the same session — statefulness compounds with it.\n\n### Discovering what's stashed: use globals()\n\nAt the start of any follow-up call, run `globals()` to see what data is already stashed from prior calls. The helper returns `{ name: summary }` for every user-added global (e.g. `{ topCampaigns: 'Array(5)', metricId: 'string(8 chars)' }`). This is the cheap way to avoid re-fetching: check what's there before fetching again.\n\n```js\n// Start of a follow-up call\nconst stashed = globals();\nif (stashed.topCampaigns) {\n // reuse globalThis.topCampaigns — skip the re-fetch\n return globalThis.topCampaigns;\n}\n```\n\n### Sharing data: use globalThis\n\nBecause user code runs as a fresh script per call, top-level `const` / `let` declarations are **scoped to that call only** — they're NOT visible later. Use `globalThis` (or `globalThis.x = ...` shorthand) for anything you want to carry forward.\n\n```js\n// Call 1\nconst metricId = await klaviyo.getConversionMetricId();\nglobalThis.metricId = metricId; // persisted\nconst topCampaigns = topN(\n (await klaviyo.reporting.campaignValues({\n data: { type: 'campaign-values-report', attributes: {\n timeframe: { key: 'last_30_days' },\n conversion_metric_id: metricId,\n statistics: ['conversion_value', 'recipients']\n }}\n })).data.attributes.results,\n 5,\n (r) => r.statistics.conversion_value\n);\nglobalThis.topCampaigns = topCampaigns; // persisted\nreturn pick(topCampaigns, { groupings: { campaign_id: true }, statistics: { conversion_value: true } });\n```\n\n```js\n// Call 2 — no re-fetch needed\nconst topIds = globalThis.topCampaigns.map(c => c.groupings.campaign_id);\nconst details = await Promise.all(topIds.map(id =>\n klaviyo.campaigns.get(id, { 'fields[campaign]': 'name,send_time' })\n));\nreturn details.map(d => ({ id: d.data.id, name: d.data.attributes.name }));\n```\n\n### Session reset (TTL + memory)\n\nThe context is recreated when any of the following happens:\n- The MCP connection closes (Claude Desktop is quit or the extension is disabled)\n- 30 minutes of idle time pass with no `execute_code` call (configurable via env)\n- Memory usage exceeds the per-isolate cap (256 MB)\n- The user manually reloads the extension\n\nWhen the context is recreated, the next call's response includes `sessionReset: true` and a `sessionResetNote`. If you see that, any globals you set previously are gone — re-declare what you need.\n\n```js\n// You can detect a reset by checking globalThis.\nif (typeof globalThis.cachedMetricId === 'undefined') {\n globalThis.cachedMetricId = await klaviyo.getConversionMetricId();\n}\nconst metricId = globalThis.cachedMetricId;\n```\n\n### Why this beats Stainless's stateless model\n\nStainless's Cloudflare-Workers sandbox runs each `execute` in a fresh isolate. For iterative analyses they require re-fetching at every step, which inflates token cost AND duration. Our self-hosted sidecar trivially supports state because it owns the isolate lifecycle.",
84
99
  "tags": [
85
100
  "stateful",
86
101
  "sessions",
@@ -108,7 +123,7 @@
108
123
  "platform": "klaviyo",
109
124
  "category": "method",
110
125
  "summary": "List campaigns (sugar for klaviyo.get('campaigns', ...)).",
111
- "content": "## klaviyo.campaigns.list(params?)\n\nSee https://developers.klaviyo.com/en/reference/get_campaigns. Filter syntax required — at minimum a `filter` param scoping to a channel.\n\n```js\nconst { data } = await klaviyo.campaigns.list({\n filter: 'equals(messages.channel,\"email\")',\n sort: '-send_time',\n 'page[size]': '20',\n 'fields[campaign]': 'name,status,send_time',\n});\nreturn data.map(c => ({ id: c.id, ...c.attributes }));\n```",
126
+ "content": "## klaviyo.campaigns.list(params?)\n\nSee https://developers.klaviyo.com/en/reference/get_campaigns. Filter syntax required — at minimum a `filter` param scoping to a channel.\n\nValid `sort` values: `created_at`, `updated_at`, `scheduled_at`, `id`, `name` (prefix with `-` for descending). NOTE: `send_time` is a response field but is NOT a valid sort key — use `-scheduled_at` to get the most-recently-sent campaigns first.\n\n```js\nconst { data } = await klaviyo.campaigns.list({\n filter: 'equals(messages.channel,\"email\")',\n sort: '-scheduled_at',\n 'page[size]': '20',\n 'fields[campaign]': 'name,status,send_time,scheduled_at',\n});\nreturn data.map(c => ({ id: c.id, ...c.attributes }));\n```",
112
127
  "tags": [
113
128
  "klaviyo",
114
129
  "campaigns",
@@ -4258,7 +4273,7 @@
4258
4273
  "platform": "klaviyo",
4259
4274
  "category": "method",
4260
4275
  "summary": "Campaign reporting (revenue, opens, clicks). Cached 10 min.",
4261
- "content": "## klaviyo.reporting.campaignValues(body)\n\nWrapper for `POST /campaign-values-reports`. Reporting tier — rate limited at 1/s, 2/min. Results cached 10 minutes.\n\n```js\nconst metricId = await klaviyo.getConversionMetricId();\nconst report = await klaviyo.reporting.campaignValues({\n data: { type: 'campaign-values-report', attributes: {\n timeframe: { key: 'last_30_days' },\n conversion_metric_id: metricId,\n statistics: ['recipients', 'opens', 'open_rate', 'clicks', 'click_rate', 'conversion_value', 'conversions'],\n }}\n});\nreturn report.data.attributes.results\n .sort((a, b) => b.statistics.conversion_value - a.statistics.conversion_value)\n .slice(0, 5);\n```",
4276
+ "content": "## klaviyo.reporting.campaignValues(body)\n\nWrapper for `POST /campaign-values-reports`. Reporting tier — rate limited at 1/s, 2/min. Results cached 10 minutes.\n\n**Response grain (important).** Each row in `report.data.attributes.results` is keyed by `(campaign_id, campaign_message_id, send_channel)`, NOT by campaign. A campaign with multiple message variations (A/B tests, multi-channel sends) produces multiple rows. For campaign-level totals (revenue, recipients, conversions) you MUST sum the `statistics.*` fields across all rows sharing the same `campaign_id` before sorting. A naive `sort + slice` of raw rows ranks individual messages, not campaigns.\n\n```js\nconst metricId = await klaviyo.getConversionMetricId();\nconst report = await klaviyo.reporting.campaignValues({\n data: { type: 'campaign-values-report', attributes: {\n timeframe: { key: 'last_30_days' },\n conversion_metric_id: metricId,\n statistics: ['recipients', 'opens', 'open_rate', 'clicks', 'click_rate', 'conversion_value', 'conversions'],\n }}\n});\n\n// Aggregate per campaign_id (sum revenue/recipients/etc., weighted-mean rates).\nconst byCampaign = new Map();\nfor (const row of report.data.attributes.results || []) {\n const id = row.groupings.campaign_id;\n const slot = byCampaign.get(id) ?? { campaign_id: id, revenue: 0, recipients: 0, opens: 0, clicks: 0 };\n slot.revenue += row.statistics.conversion_value || 0;\n slot.recipients += row.statistics.recipients || 0;\n slot.opens += row.statistics.opens || 0;\n slot.clicks += row.statistics.clicks || 0;\n byCampaign.set(id, slot);\n}\nreturn [...byCampaign.values()]\n .map(c => ({ ...c, open_rate: c.recipients ? c.opens / c.recipients : 0, click_rate: c.recipients ? c.clicks / c.recipients : 0 }))\n .sort((a, b) => b.revenue - a.revenue)\n .slice(0, 5);\n```",
4262
4277
  "tags": [
4263
4278
  "klaviyo",
4264
4279
  "reporting",
@@ -4272,7 +4287,7 @@
4272
4287
  "platform": "klaviyo",
4273
4288
  "category": "method",
4274
4289
  "summary": "Flow reporting (revenue, opens, clicks). Cached 10 min.",
4275
- "content": "## klaviyo.reporting.flowValues(body)\n\nSame shape as campaignValues but for flows. `POST /flow-values-reports`.\n\n```js\nconst metricId = await klaviyo.getConversionMetricId();\nconst report = await klaviyo.reporting.flowValues({\n data: { type: 'flow-values-report', attributes: {\n timeframe: { key: 'last_30_days' },\n conversion_metric_id: metricId,\n statistics: ['recipients', 'click_rate', 'conversion_rate', 'conversion_value'],\n }}\n});\n```",
4290
+ "content": "## klaviyo.reporting.flowValues(body)\n\nSame shape as campaignValues but for flows. `POST /flow-values-reports`. Reporting tier — rate limited at 1/s, 2/min. Results cached 10 minutes.\n\n**Response grain (important).** Each row in `report.data.attributes.results` is keyed by `(flow_id, flow_message_id, send_channel)`, NOT by flow. A typical welcome / abandoned-cart flow has 5-10 messages → 5-10 rows per flow. For flow-level totals (revenue, recipients) you MUST sum the `statistics.*` fields across all rows sharing the same `flow_id` before sorting. A naive `sort + slice` of raw rows ranks individual messages-within-flows, not flows.\n\n```js\nconst metricId = await klaviyo.getConversionMetricId();\nconst report = await klaviyo.reporting.flowValues({\n data: { type: 'flow-values-report', attributes: {\n timeframe: { key: 'last_30_days' },\n conversion_metric_id: metricId,\n statistics: ['recipients', 'opens_unique', 'click_rate', 'conversion_rate', 'conversion_value'],\n }}\n});\n\n// Aggregate per flow_id (correct flow-level totals).\nconst byFlow = new Map();\nfor (const row of report.data.attributes.results || []) {\n const id = row.groupings.flow_id;\n const slot = byFlow.get(id) ?? { flow_id: id, revenue: 0, recipients: 0, opens_unique: 0 };\n slot.revenue += row.statistics.conversion_value || 0;\n slot.recipients += row.statistics.recipients || 0;\n slot.opens_unique += row.statistics.opens_unique || 0;\n byFlow.set(id, slot);\n}\nreturn [...byFlow.values()]\n .map(f => ({ ...f, open_rate: f.recipients ? f.opens_unique / f.recipients : 0 }))\n .sort((a, b) => b.revenue - a.revenue)\n .slice(0, 5);\n```",
4276
4291
  "tags": [
4277
4292
  "klaviyo",
4278
4293
  "reporting",
package/dist/index.js CHANGED
@@ -17,5 +17,5 @@ server.connect(transport).catch((err) => {
17
17
  console.error("[dtc-mcp] fatal: failed to connect:", err);
18
18
  process.exit(1);
19
19
  });
20
- console.error("[dtc-mcp] v1.0.3 ready");
20
+ console.error("[dtc-mcp] v1.0.4 ready");
21
21
  //# sourceMappingURL=index.js.map
@@ -94,5 +94,59 @@ globalThis.summarize = function summarize(arr, opts) {
94
94
  }
95
95
  return result;
96
96
  };
97
+
98
+ /**
99
+ * globals() — introspect what's currently stashed on globalThis.
100
+ *
101
+ * Returns { name: summary } for every user-added global, filtering out the
102
+ * sandbox's built-in helpers and standard JS globals. Use at the start of a
103
+ * follow-up turn to see what data is already available from prior calls and
104
+ * avoid re-fetching anything that's already there.
105
+ *
106
+ * // Call 1
107
+ * globalThis.flowReport = await klaviyo.reporting.flowValues({...});
108
+ *
109
+ * // Call 2 (next turn) — check what's stashed before re-fetching
110
+ * const stashed = globals();
111
+ * // → { flowReport: 'Object(2 keys)' }
112
+ *
113
+ * Implementation: captures the baseline of Object.keys(globalThis) at module
114
+ * load time via an IIFE closure. Anything added after bootstrap is "user
115
+ * state" and shows up in the listing.
116
+ */
117
+ (function () {
118
+ const __baseline = new Set(Object.keys(globalThis));
119
+ globalThis.globals = function globals() {
120
+ const out = {};
121
+ for (const k of Object.keys(globalThis)) {
122
+ if (__baseline.has(k)) continue;
123
+ if (k === 'globals') continue;
124
+ if (k.startsWith('__')) continue;
125
+ let summary;
126
+ try {
127
+ const v = globalThis[k];
128
+ if (v === null || v === undefined) {
129
+ summary = String(v);
130
+ } else if (Array.isArray(v)) {
131
+ summary = 'Array(' + v.length + ')';
132
+ } else if (typeof v === 'object') {
133
+ summary = 'Object(' + Object.keys(v).length + ' keys)';
134
+ } else if (typeof v === 'string') {
135
+ summary = v.length > 40
136
+ ? 'string(' + v.length + ' chars)'
137
+ : JSON.stringify(v);
138
+ } else if (typeof v === 'function') {
139
+ summary = 'function';
140
+ } else {
141
+ summary = typeof v + ': ' + String(v);
142
+ }
143
+ } catch (_e) {
144
+ summary = '<unreadable>';
145
+ }
146
+ out[k] = summary;
147
+ }
148
+ return out;
149
+ };
150
+ })();
97
151
  `.trim();
98
152
  //# sourceMappingURL=sandbox-helpers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox-helpers.js","sourceRoot":"","sources":["../../src/sandbox/sandbox-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFrC,CAAC,IAAI,EAAE,CAAC"}
1
+ {"version":3,"file":"sandbox-helpers.js","sourceRoot":"","sources":["../../src/sandbox/sandbox-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyIrC,CAAC,IAAI,EAAE,CAAC"}
package/dist/server.js CHANGED
@@ -5,7 +5,7 @@ import { registerReadDoc } from "./tools/read_doc.js";
5
5
  export function createServer() {
6
6
  const server = new McpServer({
7
7
  name: "dtc-mcp",
8
- version: "1.0.3",
8
+ version: "1.0.4",
9
9
  });
10
10
  registerExecuteCode(server);
11
11
  registerSearchDocs(server);
@@ -3,7 +3,7 @@ import { runSandbox } from "../sandbox/runner.js";
3
3
  import { resolveTimeout } from "../sandbox/timeout.js";
4
4
  import { log } from "../config.js";
5
5
  const codeShape = {
6
- code: z.string().describe("TypeScript-like JavaScript to execute. Wrap top-level await calls naturally — the code runs in an async context. Return a value via `return ...` to receive it as the tool result. Globals available: `klaviyo`, `shopify`, `console`. No `fetch`/`process`/`require`/`import`. Add `// @timeout 2m` (max 5m) at the top to extend the default 30s wall-clock limit. Discover SDK methods via the `search_docs` tool."),
6
+ code: z.string().describe("TypeScript-like JavaScript to execute. Wrap top-level await calls naturally — the code runs in an async context. Return a value via `return ...` to receive it as the tool result. Globals available: `klaviyo`, `shopify`, `console`, plus helpers `pick`, `topN`, `summarize`, `globals`. No `fetch`/`process`/`require`/`import`. Add `// @timeout 2m` (max 5m) at the top to extend the default 30s wall-clock limit. Discover SDK methods via the `search_docs` tool."),
7
7
  };
8
8
  const description = `
9
9
  Execute JavaScript against the typed Klaviyo + Shopify SDKs in a stateful V8 sandbox.
@@ -12,11 +12,18 @@ The host applies rate limits, auth, and caching transparently. The sandbox keeps
12
12
  context alive per MCP connection — variables you assign to globalThis persist across
13
13
  calls, so iterative analyses don't re-fetch.
14
14
 
15
+ STRONGLY RECOMMENDED for multi-turn investigations: stash any expensive fetch
16
+ (reporting payloads, paginated lists, computed aggregates) on globalThis so
17
+ follow-up turns reference the stashed data instead of re-fetching it. Re-running
18
+ a 5,000-row report costs ~30k tokens; reading globalThis.report costs near zero.
19
+ Call \`globals()\` at the start of any follow-up call to see what's already stashed.
20
+
15
21
  Available globals:
16
22
  - klaviyo: { get, post, paginate, campaigns, flows, lists, segments, profiles, events, metrics, reporting }
17
23
  - shopify: { gql, ql, timezone } — Shopify Admin GraphQL + ShopifyQL
18
24
  - console: { log, error, warn, info } — captured and returned as stdout
19
25
  - pick(value, schema) / topN(arr, n, by) / summarize(arr, opts) — output-discipline helpers
26
+ - globals() — returns { name: summary } of everything currently stashed on globalThis
20
27
  - globalThis.* — assignments persist across calls within this MCP session
21
28
 
22
29
  Discovery: use read_doc({}) at session start to list every available SDK path, then
@@ -1 +1 @@
1
- {"version":3,"file":"execute_code.js","sourceRoot":"","sources":["../../src/tools/execute_code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAEnC,MAAM,SAAS,GAAG;IAChB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CACvB,uZAAuZ,CACxZ;CACF,CAAC;AAEF,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCnB,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,UAAU,mBAAmB,CAAC,MAAiB;IACnD,MAAM,CAAC,IAAI,CACT,cAAc,EACd,WAAW,EACX,SAAS,EACT;QACE,KAAK,EAAE,0BAA0B;QACjC,YAAY,EAAE,KAAK;QACnB,eAAe,EAAE,KAAK;QACtB,cAAc,EAAE,KAAK;QACrB,aAAa,EAAE,IAAI;KACpB,EACD,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;QACjB,MAAM,SAAS,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACvC,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QAEjE,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;QAErD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CACzB;YACE,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;YACpE,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,GAAG,CAAC,MAAM,CAAC,YAAY;gBACrB,CAAC,CAAC;oBACE,YAAY,EAAE,IAAI;oBAClB,gBAAgB,EACd,8SAA8S;iBACjT;gBACH,CAAC,CAAC,EAAE,CAAC;SACR,EACD,IAAI,EACJ,CAAC,CACF,CAAC;QAEF,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;YACjC,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE;SACpB,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"execute_code.js","sourceRoot":"","sources":["../../src/tools/execute_code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAEnC,MAAM,SAAS,GAAG;IAChB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CACvB,4cAA4c,CAC7c;CACF,CAAC;AAEF,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CnB,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,UAAU,mBAAmB,CAAC,MAAiB;IACnD,MAAM,CAAC,IAAI,CACT,cAAc,EACd,WAAW,EACX,SAAS,EACT;QACE,KAAK,EAAE,0BAA0B;QACjC,YAAY,EAAE,KAAK;QACnB,eAAe,EAAE,KAAK;QACtB,cAAc,EAAE,KAAK;QACrB,aAAa,EAAE,IAAI;KACpB,EACD,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;QACjB,MAAM,SAAS,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACvC,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QAEjE,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;QAErD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CACzB;YACE,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;YACpE,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,GAAG,CAAC,MAAM,CAAC,YAAY;gBACrB,CAAC,CAAC;oBACE,YAAY,EAAE,IAAI;oBAClB,gBAAgB,EACd,8SAA8S;iBACjT;gBACH,CAAC,CAAC,EAAE,CAAC;SACR,EACD,IAAI,EACJ,CAAC,CACF,CAAC;QAEF,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;YACjC,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE;SACpB,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dtc-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Code-execution MCP server for Klaviyo + Shopify analytics. The LLM writes TypeScript against typed SDKs in a stateful V8 sandbox — three composable tools instead of dozens.",
5
5
  "type": "module",
6
6
  "bin": {