Skip to content

Commit 305103b

Browse files
author
clamp-bot
committed
sync from monorepo @ d4974ff
1 parent 786231a commit 305103b

2 files changed

Lines changed: 73 additions & 18 deletions

File tree

src/index.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,30 @@ function formatApiError(status: number, body: string, path: string): string {
8989

9090
const boot = await bootstrap();
9191

92-
const server = new McpServer({ name: "clamp", version: "0.1.0" });
92+
// The `instructions` string is injected into the agent's system prompt at
93+
// connection time per the MCP spec. Use it to nudge skill loading rather
94+
// than relying purely on each skill's semantic match.
95+
const SERVER_INSTRUCTIONS = `Clamp Analytics MCP. Use these tools to read pageviews, custom events, funnels, cohorts, retention, revenue, errors, and user journeys for a single project.
96+
97+
Before INTERPRETING any analytics result from these tools, load the relevant analytics-skills skill(s). Skills enforce methodology — sample-size discipline, Simpson's paradox detection, causal-reasoning hygiene, mix-shift checks — that pure tool calls cannot.
98+
99+
Skill loading map:
100+
- Any analytics interpretation question → load \`analytics-skills:analytics-diagnostic-method\` (the spine; load this first).
101+
- "Why did traffic change / drop / spike?" → also load \`analytics-skills:traffic-change-diagnosis\`.
102+
- "Is this metric good? Is bounce/CVR/churn normal?" → load \`analytics-skills:metric-context-and-benchmarks\`.
103+
- Funnel reading, channel comparison, "which is best" → load \`analytics-skills:channel-and-funnel-quality\`.
104+
- A/B test reading, "did the variant win?" → load \`analytics-skills:experiment-result-reader\` (+ \`bayesian-experiment-reader\` for posterior P(better)/expected-loss; + \`sequential-monitoring\` for safe peeking).
105+
- "Did X cause Y?" on observational data (no holdback) → load \`analytics-skills:causal-query-classifier\` first to rung-tag the question; then \`causal-dag-builder\` to make assumptions explicit; then \`causal-evidence-checklist\` (Bradford Hill) before recommending an action.
106+
- Cohort comparison, funnel-by-cohort, "this segment converts X% higher" → load \`analytics-skills:causal-dag-builder\` to surface confounders before declaring causation.
107+
- Time-series anomaly questions, contested change dates, two fingerprints matching → load \`analytics-skills:anomaly-detection-time-series\`.
108+
- First time talking to a new project → run \`analytics-skills:analytics-profile-setup\` once so subsequent answers calibrate to the user's industry and business model.
109+
110+
If the user is interpreting a number AND no analytics-skills skill has been loaded, prefer to load the relevant skill and re-read with discipline rather than answer ad-hoc. The skill descriptions specify their triggers; match by question shape, not vendor.`;
111+
112+
const server = new McpServer(
113+
{ name: "clamp", version: "0.1.0" },
114+
{ instructions: SERVER_INSTRUCTIONS },
115+
);
93116
registerClampTools(server, {
94117
api,
95118
projects: [{ id: boot.projectId, name: boot.projectName, plan: boot.plan }],

src/tools.ts

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,45 @@ function qs(params: Record<string, string | number | boolean | undefined>): stri
179179
* wrap their payload in a meaningful key (rows, events, series, etc.) before
180180
* calling this.
181181
*/
182-
function out(data: unknown) {
182+
function out(data: unknown, methodologyHint?: string) {
183+
const jsonText = JSON.stringify(data, null, 2);
184+
// Prepend a methodology hint when the calling tool is one whose results
185+
// need interpretation discipline. The hint reaches the agent inside the
186+
// active turn's tool result — strictly more reliable than the server's
187+
// top-level `instructions` for nudging skill loading at point-of-use.
188+
// structuredContent stays clean so programmatic consumers aren't disturbed.
189+
const text = methodologyHint
190+
? `<methodology>\n${methodologyHint}\n</methodology>\n\n${jsonText}`
191+
: jsonText;
183192
return {
184-
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
193+
content: [{ type: "text" as const, text }],
185194
structuredContent: data as Record<string, unknown>,
186195
};
187196
}
188197

198+
// ── Methodology hints (prepended to diagnostic tool results) ──
199+
// Each hint names the analytics-skills that the agent should load before
200+
// interpreting the result. Vendor-neutral methodology lives in the skills;
201+
// these strings are the per-tool routing nudges.
202+
203+
const M_TRAFFIC =
204+
"Load `analytics-skills:analytics-diagnostic-method` before interpreting (sample-size discipline + Simpson's paradox). For 'why did X change' questions also load `analytics-skills:traffic-change-diagnosis`. For series spanning >14 days where the change date is contested, load `analytics-skills:anomaly-detection-time-series`.";
205+
206+
const M_FUNNEL =
207+
"Load `analytics-skills:channel-and-funnel-quality` before reading these drop-offs (expected step-type ranges + mix-shift detection). Pair with `analytics-skills:analytics-diagnostic-method`. If you are about to recommend an action based on a cohort delta, load `analytics-skills:causal-dag-builder` to make confounders explicit.";
208+
209+
const M_COHORT =
210+
"Load `analytics-skills:causal-dag-builder` before claiming one cohort 'caused' a difference — observational cohort splits have intent + selection confounds. Pair with `analytics-skills:analytics-diagnostic-method`. For Bradford-Hill grading before a ship/rollback decision, load `analytics-skills:causal-evidence-checklist`.";
211+
212+
const M_REVENUE =
213+
"Load `analytics-skills:metric-context-and-benchmarks` for revenue interpretation (LTV/CAC, ARPU benchmarks by model). Pair with `analytics-skills:analytics-diagnostic-method` for mix-shift and sample-size discipline. Currencies are not auto-converted; the response returns one bucket per currency.";
214+
215+
const M_ERROR =
216+
"Load `analytics-skills:analytics-diagnostic-method` before triaging (one cause, not five guesses). Errors deduplicated by fingerprint; correlate spike windows with deploy timestamps before recommending a rollback.";
217+
218+
const M_ENGAGEMENT =
219+
"Load `analytics-skills:metric-context-and-benchmarks` for per-page-type expected ranges (a 'high' bounce on a docs page is healthy; on a pricing page it isn't). Pair with `analytics-skills:analytics-diagnostic-method`.";
220+
189221
// ── Output schemas ───────────────────────────────────
190222
// Declared on every tool via registerTool's outputSchema. The SDK validates
191223
// each handler's structuredContent against the matching shape, and clients
@@ -713,7 +745,7 @@ Limitations: bounce_rate and avg_duration are derived from the SDK's pageview_en
713745
async ({ project_id, ...rest }) => {
714746
const p = resolveProject(project_id);
715747
if (isErr(p)) return p;
716-
return out(await api(`/analytics/${p.projectId}/overview${qs(rest)}`));
748+
return out(await api(`/analytics/${p.projectId}/overview${qs(rest)}`), M_TRAFFIC);
717749
},
718750
);
719751

@@ -977,7 +1009,7 @@ Limitations: events without any Money property contribute zero. If \`property\`
9771009
const p = resolveProject(project_id);
9781010
if (isErr(p)) return p;
9791011
const data = (await api(`/analytics/${p.projectId}/revenue${qs(rest)}`)) as unknown[];
980-
return out({ rows: data });
1012+
return out({ rows: data }, M_REVENUE);
9811013
},
9821014
);
9831015

@@ -1022,7 +1054,7 @@ Pairs with: \`revenue.sum(attribution_model="first_touch")\` to validate the agg
10221054
async ({ project_id, ...rest }) => {
10231055
const p = resolveProject(project_id);
10241056
if (isErr(p)) return p;
1025-
return out(await api(`/analytics/${p.projectId}/users/journey${qs(rest)}`));
1057+
return out(await api(`/analytics/${p.projectId}/users/journey${qs(rest)}`), M_TRAFFIC);
10261058
},
10271059
);
10281060

@@ -1088,7 +1120,7 @@ Pairs with: \`errors.groups\` (find a noisy fingerprint, then list its occurrenc
10881120
async ({ project_id, ...rest }) => {
10891121
const p = resolveProject(project_id);
10901122
if (isErr(p)) return p;
1091-
return out(await api(`/analytics/${p.projectId}/errors${qs(rest)}`));
1123+
return out(await api(`/analytics/${p.projectId}/errors${qs(rest)}`), M_ERROR);
10921124
},
10931125
);
10941126

@@ -1124,7 +1156,7 @@ Pairs with: \`errors.list\` (drill into a fingerprint to see individual occurren
11241156
async ({ project_id, ...rest }) => {
11251157
const p = resolveProject(project_id);
11261158
if (isErr(p)) return p;
1127-
return out(await api(`/analytics/${p.projectId}/errors/groups${qs(rest)}`));
1159+
return out(await api(`/analytics/${p.projectId}/errors/groups${qs(rest)}`), M_ERROR);
11281160
},
11291161
);
11301162

@@ -1164,7 +1196,7 @@ Pairs with: \`errors.groups\` (find which fingerprint is worth charting); \`traf
11641196
async ({ project_id, ...rest }) => {
11651197
const p = resolveProject(project_id);
11661198
if (isErr(p)) return p;
1167-
return out(await api(`/analytics/${p.projectId}/errors/timeline${qs(rest)}`));
1199+
return out(await api(`/analytics/${p.projectId}/errors/timeline${qs(rest)}`), M_ERROR);
11681200
},
11691201
);
11701202

@@ -1209,7 +1241,7 @@ Pairs with: \`errors.list\` (source of the anonymous_id and timestamp pair); \`u
12091241
async ({ project_id, ...rest }) => {
12101242
const p = resolveProject(project_id);
12111243
if (isErr(p)) return p;
1212-
return out(await api(`/analytics/${p.projectId}/errors/context${qs(rest)}`));
1244+
return out(await api(`/analytics/${p.projectId}/errors/context${qs(rest)}`), M_ERROR);
12131245
},
12141246
);
12151247

@@ -1259,7 +1291,7 @@ Limitations: \`metric\` and \`event\` are mutually exclusive — when \`metric\`
12591291
const p = resolveProject(project_id);
12601292
if (isErr(p)) return p;
12611293
const data = (await api(`/analytics/${p.projectId}/timeseries${qs(rest)}`)) as unknown[];
1262-
return out({ series: data });
1294+
return out({ series: data }, M_TRAFFIC);
12631295
},
12641296
);
12651297

@@ -1343,7 +1375,7 @@ Limitations: returns 404 if no funnel exists by that name — call funnels.list
13431375
if (isErr(p)) return p;
13441376
const data = await api(`/analytics/${p.projectId}/funnels${qs(rest)}`);
13451377
const funnels = Array.isArray(data) ? data : [data];
1346-
return out({ funnels });
1378+
return out({ funnels }, M_FUNNEL);
13471379
},
13481380
);
13491381

@@ -1499,7 +1531,7 @@ Pairs with: \`cohorts.compare\` to stack 2–10 cohorts side-by-side at the same
14991531
async ({ project_id, name, periods }) => {
15001532
const p = resolveProject(project_id);
15011533
if (isErr(p)) return p;
1502-
return out(await api(`/analytics/${p.projectId}/cohorts/${encodeURIComponent(name)}/retention${qs({ periods })}`));
1534+
return out(await api(`/analytics/${p.projectId}/cohorts/${encodeURIComponent(name)}/retention${qs({ periods })}`), M_COHORT);
15031535
},
15041536
);
15051537

@@ -1538,7 +1570,7 @@ Limitations: at most 10 cohorts per call. The same retention windows are applied
15381570
async ({ project_id, ...rest }) => {
15391571
const p = resolveProject(project_id);
15401572
if (isErr(p)) return p;
1541-
return out(await api(`/analytics/${p.projectId}/cohorts/compare${qs(rest)}`));
1573+
return out(await api(`/analytics/${p.projectId}/cohorts/compare${qs(rest)}`), M_COHORT);
15421574
},
15431575
);
15441576

@@ -1710,7 +1742,7 @@ Limitations: aggregates pageview events only — for custom event breakdowns use
17101742
const p = resolveProject(project_id);
17111743
if (isErr(p)) return p;
17121744
const data = (await api(`/analytics/${p.projectId}/breakdown${qs(rest)}`)) as unknown[];
1713-
return out({ rows: data });
1745+
return out({ rows: data }, M_TRAFFIC);
17141746
},
17151747
);
17161748

@@ -1744,7 +1776,7 @@ Limitations: one metric per call — for multi-metric comparison either call rep
17441776
async ({ project_id, ...rest }) => {
17451777
const p = resolveProject(project_id);
17461778
if (isErr(p)) return p;
1747-
return out(await api(`/analytics/${p.projectId}/compare${qs(rest)}`));
1779+
return out(await api(`/analytics/${p.projectId}/compare${qs(rest)}`), M_TRAFFIC);
17481780
},
17491781
);
17501782

@@ -1780,7 +1812,7 @@ Limitations: shows entry and exit only, not the full pageview chain in between (
17801812
const p = resolveProject(project_id);
17811813
if (isErr(p)) return p;
17821814
const data = (await api(`/analytics/${p.projectId}/session-paths${qs(rest)}`)) as unknown[];
1783-
return out({ paths: data });
1815+
return out({ paths: data }, M_TRAFFIC);
17841816
},
17851817
);
17861818

@@ -1820,7 +1852,7 @@ Limitations: avg_engagement_seconds is null for pages without pageview_end data
18201852
if (isErr(p)) return p;
18211853
const data = (await api(`/analytics/${p.projectId}/engagement${qs(rest)}`)) as unknown[];
18221854
const wrapper = (rest as { view?: string }).view === "sections" ? { sections: data } : { pages: data };
1823-
return out(wrapper);
1855+
return out(wrapper, M_ENGAGEMENT);
18241856
},
18251857
);
18261858

0 commit comments

Comments
 (0)