marketing-mindset-mcp 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +28 -0
  2. package/bin/server.js +134 -7
  3. package/package.json +11 -8
package/README.md CHANGED
@@ -33,6 +33,8 @@ Client config (generic stdio server):
33
33
  | `read_section` | one section by fuzzy title match (`"numbers lie"`, `"competitors"`, `"hypotheses"`) |
34
34
  | `sample_size` | per-arm volume a two-proportion test needs before a verdict is readable |
35
35
  | `kill_rule` | expected conversions, the kill floor, and whether the test has enough volume to read yet |
36
+ | `aeo_check` | agent-readiness / AEO audit: the 23 weighted checks that decide whether AI answers can find, read and quote a site; pass the numbers you pass to get a weighted score |
37
+ | `jd` | a marketing-engineer job description about artifacts and numbers: what the role owns, what must exist by day 90, how it is measured, screening questions |
36
38
 
37
39
  `sample_size` and `kill_rule` implement the same two-proportion arithmetic as the free
38
40
  [test-size calculator](https://axelfreeman.github.io/marketing-mindset/tools/email-test-planner.html).
@@ -44,6 +46,32 @@ tools/call sample_size {"baseline_rate": 0.03, "min_relative_lift": 0.2}
44
46
  -> per_arm 13914 (i.e. ~13.9k sends per variant before any verdict)
45
47
  ```
46
48
 
49
+
50
+ ## Example: the AEO audit
51
+
52
+ ```
53
+ tools/call aeo_check {}
54
+ -> 23 checks, max score 55, grouped into crawlability, answer-ready content, machine-readable, someone else's page, proof and measurement
55
+
56
+ tools/call aeo_check {"checked": [1,2,4,6,8,11,12,16,20,21,22]}
57
+ -> score, band and the highest-weight gaps
58
+ ```
59
+
60
+ The same checklist is a free page: [agent-readiness checklist](https://axelfreeman.github.io/marketing-mindset/tools/aeo-readiness-checklist.html). The JD tool has a web version too: [JD generator](https://axelfreeman.github.io/marketing-mindset/tools/marketing-engineer-jd-generator.html).
61
+
47
62
  ## License
48
63
 
49
64
  MIT.
65
+
66
+ ## Work with me
67
+
68
+ This package is the method, packaged. The done-for-you version — offer page, query-intent source pages,
69
+ schema and `llms.txt`, a free tool, deliverability setup, tests with a written kill rule, and the tracked
70
+ event in your own analytics property — is built in fixed packages:
71
+ [Sprint $900 one-time, Engine $1,900/month, Full build $2,900](https://axelfreeman.com/marketing-engineer.html).
72
+
73
+ Live artifacts from the same engine, all openable:
74
+ [proof page](https://axelfreeman.com/proof.html) ·
75
+ [query-intent pages](https://axelfreeman.com/hire-marketing-engineer.html) ·
76
+ [free test-size calculator](https://axelfreeman.github.io/marketing-mindset/tools/email-test-planner.html) ·
77
+ [Telegram](https://t.me/AxelFreeman).
package/bin/server.js CHANGED
@@ -9,7 +9,7 @@ const fs = require("fs");
9
9
  const path = require("path");
10
10
 
11
11
  const SKILL_PATH = path.join(__dirname, "..", "skill", "SKILL.md");
12
- const SERVER = { name: "marketing-mindset-mcp", version: "1.0.0" };
12
+ const SERVER = { name: "marketing-mindset-mcp", version: "1.0.2" };
13
13
  const PROTOCOL = "2024-11-05";
14
14
 
15
15
  function skillText() {
@@ -63,16 +63,116 @@ function killRule(rate, n, lift) {
63
63
  const floor = Math.max(0, Math.floor(bound));
64
64
  const expected = rate * n;
65
65
  const target = rate * (1 + lift) * n;
66
- let verdict = "keep running — not enough volume to read the result";
66
+ let verdict = "keep running - not enough volume to read the result";
67
67
  if (n < sampleSize(rate, lift, 0.8, 0.975)) {
68
- verdict = "keep running — below the required volume for this effect size, a verdict now is noise";
68
+ verdict = "keep running - below the required volume for this effect size, a verdict now is noise";
69
69
  } else {
70
- verdict = "read it — volume is sufficient; kill if conversions are at or under the floor";
70
+ verdict = "read it - volume is sufficient; kill if conversions are at or under the floor";
71
71
  }
72
72
  return { expected_conversions: Math.round(expected * 10) / 10, kill_floor: floor,
73
73
  target_conversions: Math.round(target * 10) / 10, verdict };
74
74
  }
75
75
 
76
+ /* --- agent readiness / AEO checklist: 5 groups, 23 checks, 55 points ------------------------ */
77
+ const AEO_GROUPS = [["Crawlability",[["robots.txt does not block GPTBot, ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot",3],["Pages render without JavaScript, or have a server-side/no-JS fallback",2],["No login wall or consent interstitial in front of the content you want quoted",3],["The page returns 200 to a plain curl with no cookies",2],["One canonical URL per answer - no www/non-www or trailing-slash duplicates",2]]],["Answer-ready content",[["Every important page answers one question in the first 60 words",3],["Headings are questions a human would type, not internal project names",2],["Prices, limits and timeframes are stated as numbers, not 'flexible'",3],["Claims have a source or a live URL attached",2],["A definition page exists for the category you want to be cited in",3]]],["Machine-readable",[["schema.org markup on service/product pages (Service, Offer, Product, FAQPage)",3],["FAQPage or QAPage markup on question pages, answers 40-500 words",2],["XML sitemap submitted, and every new URL pinged via IndexNow",2],["llms.txt describes what the site answers and where the canonical pages are",2],["Organisation/Person entity links the page to a real, verifiable identity",2]]],["Someone else's page",[["You are present where the model already reads: package registries, repos, docs",3],["At least one open, mergeable pull request or discussion contribution",2],["Your content is mirrored somewhere you do not control",2],["A third-party page links to your canonical URL",3]]],["Proof and measurement",[["Every claim on the site can be opened by the reader in one click",3],["One tracked event for the outcome that matters, in your own analytics property",3],["A public changelog or artifact list with dates",2],["An archived snapshot of the key pages exists",1]]]];
78
+ const AEO_MAX = AEO_GROUPS.reduce((a, g) => a + g[1].reduce((b, c) => b + c[1], 0), 0);
79
+
80
+ function aeoFlat() {
81
+ const out = [];
82
+ AEO_GROUPS.forEach(([group, items]) => items.forEach(([text, weight]) => out.push({ group, check: text, weight })));
83
+ return out;
84
+ }
85
+
86
+ function aeoCheck(checked) {
87
+ const flat = aeoFlat();
88
+ const base = { check_count: flat.length, max_score: AEO_MAX,
89
+ groups: AEO_GROUPS.map(([g, its]) => ({ group: g, checks: its.map(([t, w]) => ({ check: t, weight: w })) })),
90
+ score_guide: "<30% invisible for structural reasons; 30-60% crawlable but not quotable; 60-80% quoted occasionally (third-party presence missing); >80% the rest is cadence" };
91
+ if (!Array.isArray(checked)) return Object.assign({}, base, { mode: "checklist" });
92
+ const idx = checked.map((v) => Number(v)).filter((n) => Number.isInteger(n) && n >= 1 && n <= flat.length);
93
+ const passed = new Set(idx);
94
+ const score = flat.reduce((a, c, i) => a + (passed.has(i + 1) ? c.weight : 0), 0);
95
+ const pct = Math.round((score / AEO_MAX) * 1000) / 10;
96
+ const missing = flat.map((c, i) => ({ n: i + 1, check: c.check, group: c.group, weight: c.weight }))
97
+ .filter((c) => !passed.has(c.n)).sort((a, b) => b.weight - a.weight).slice(0, 3);
98
+ return { mode: "score", passed: passed.size, of_checks: flat.length, score, max_score: AEO_MAX,
99
+ percent: pct,
100
+ band: pct < 30 ? "invisible for structural reasons" : pct < 60 ? "crawlable but not quotable"
101
+ : pct < 80 ? "quoted occasionally - third-party presence is the gap" : "the rest is cadence",
102
+ highest_weight_unchecked: missing,
103
+ note: "Weighted, not a checklist of equals. Two checks carry the most weight: one question answered in the first 60 words, and prices/limits stated as numbers." };
104
+ }
105
+
106
+ /* --- marketing engineer JD generator (same text as the npm CLI `jd` and the free web tool) --- */
107
+ const JD_SURFACES = {
108
+ seo: "search and answer engines (pages written to be quoted, schema, llms.txt)",
109
+ out: "outbound and lifecycle email (list, sequence, deliverability, suppression)",
110
+ paid: "paid acquisition (structure, creative, spend gates)",
111
+ auto: "automation and tracking (routing, events, failure alerts)",
112
+ analytics: "measurement (one event that matters, sample-size floors, kill rules)",
113
+ ai: "agent skills and AI-assisted production (reusable operating files, not one-off prompts)",
114
+ };
115
+ const JD_OFFER = "https://axelfreeman.com/marketing-engineer.html";
116
+ const JD_COST = "https://axelfreeman.com/marketing-engineer-cost.html";
117
+ const JD_TOOL = "https://axelfreeman.github.io/marketing-mindset/tools/marketing-engineer-jd-generator.html";
118
+
119
+ function jdText(o) {
120
+ const dflt = (v, d) => (v === undefined || v === null || v === "" || v === true) ? d : String(v);
121
+ const level = dflt(o.level, "senior");
122
+ const mode = dflt(o.mode, level === "contract" ? "contract" : "fulltime");
123
+ const sv = dflt(o.surfaces, null);
124
+ const keys = sv ? sv.split(",").map((s) => s.trim()).filter((s) => JD_SURFACES[s]) : Object.keys(JD_SURFACES);
125
+ const surf = keys.length ? keys : Object.keys(JD_SURFACES);
126
+ const company = dflt(o.company, "[company, product, what you sell in one line]");
127
+ const bottleneck = dflt(o.bottleneck, "[the bottleneck you are hiring against]");
128
+ const ninety = mode === "contract"
129
+ ? "A 6-week engagement with one written kill rule, then a decision"
130
+ : level === "lead"
131
+ ? "A documented system: which paths exist, what each one costs, what gets killed next quarter"
132
+ : "A path live in production with its event recording, or a written note saying why it was killed";
133
+ const L = [];
134
+ L.push("# Marketing Engineer (" + level + ", " + mode + ")", "");
135
+ L.push("## The company and the job");
136
+ L.push(company + ".", "");
137
+ L.push("We are hiring a marketing engineer because: " + bottleneck + ".", "");
138
+ L.push("You will not be handed a channel to run and a deck to fill in. You will be handed a broken or "
139
+ + "unmeasured path and be expected to make it work, show the number, and write the rule that decides "
140
+ + "whether it lives.", "");
141
+ L.push("## What the role owns");
142
+ surf.forEach((k, i) => L.push((i + 1) + ". " + JD_SURFACES[k]));
143
+ L.push("", "## What must exist by day 90");
144
+ L.push("1. One path live in production: entry point, sequence or flow, and the event that fires on a conversion.");
145
+ L.push("2. A written declaration of the sample size each running test needs before a result counts as a result.");
146
+ L.push("3. A kill rule per test, written before the test starts: at what volume and what result we stop.");
147
+ L.push("4. One dashboard we can open without asking you for an export.");
148
+ L.push("5. " + ninety + ".");
149
+ L.push("", "## How we will measure you");
150
+ L.push("- Cost per qualified conversation or per activated account, when the volume is above the declared floor.");
151
+ L.push("- Live, openable artifacts still running 30 days after you last touched them.");
152
+ L.push("- Whether a decision made last month can be re-checked today from the data, without you in the room.");
153
+ L.push("- We do not count posts published, prompts written, or hours logged.");
154
+ L.push("", "## Requirements");
155
+ L.push("- You have shipped something that is still running: a page, a sequence, a script, an integration. Links beat adjectives.");
156
+ L.push("- You can explain, in one paragraph, why a 4-of-100 result is not a win.");
157
+ L.push("- You work in the tooling yourself: HTML/CSS, a spreadsheet, analytics, basic scripting, or an agent that writes them for you.");
158
+ L.push("- You write plainly; internal names and jargon are not allowed in anything a customer reads.");
159
+ L.push("", "## Nice to have");
160
+ L.push("- Experience writing pages that answer engines quote (schema, FAQ markup, plain HTML).");
161
+ L.push("- A history of killing your own work in public, with the numbers attached.");
162
+ L.push("", "## Not this role");
163
+ L.push("- Meeting-led account management, community management, brand campaigns, reporting for its own sake.");
164
+ L.push("", "## Screening questions (ask these first)");
165
+ L.push("1. Send us one link to something you built that is still live.");
166
+ L.push("2. We ran 40 sends, got 4 replies, variant B looks better. What do you do next?");
167
+ L.push("3. How many sends does a two-variant email test need before the result means anything? "
168
+ + "(Expected: ~1,500-2,000 per arm at a 3% baseline, 80% power, 20% relative lift.)");
169
+ L.push("4. Name one thing you killed and what the number was.");
170
+ L.push("5. What will you build in the first two weeks here, and how will we see it?");
171
+ L.push("", "--", "Generated by marketing-mindset (npm). Method and pricing: " + JD_OFFER
172
+ + " | Packages: " + JD_COST + " | Web version of this tool: " + JD_TOOL);
173
+ return L.join("\n");
174
+ }
175
+
76
176
  const TOOLS = [
77
177
  {
78
178
  name: "list_sections",
@@ -107,17 +207,38 @@ const TOOLS = [
107
207
  min_relative_lift: { type: "number", description: "Smallest relative lift worth detecting" }
108
208
  },
109
209
  required: ["baseline_rate", "n", "min_relative_lift"], additionalProperties: false }
210
+ },
211
+ {
212
+ name: "aeo_check",
213
+ description: "Agent-readiness / AEO audit: the 23 weighted checks that decide whether AI answers can find, read and quote a site. Call with no arguments for the checklist; pass the 1-based numbers of the checks the site passes (e.g. {\"checked\":[1,2,4,6,8,11,12]}) to get the weighted score, the band and the highest-weight gaps.",
214
+ inputSchema: { type: "object",
215
+ properties: { checked: { type: "array", items: { type: "integer" },
216
+ description: "Optional: 1-based numbers of checks the site passes, out of 23" } },
217
+ additionalProperties: false }
218
+ },
219
+ {
220
+ name: "jd",
221
+ description: "Generates a marketing-engineer job description that is about artifacts and numbers instead of adjectives: what the role owns, what must exist by day 90, how it is measured, and the screening questions (including the sample-size question with the expected answer).",
222
+ inputSchema: { type: "object",
223
+ properties: {
224
+ level: { type: "string", description: "senior (default), lead, or contract" },
225
+ mode: { type: "string", description: "fulltime (default) or contract" },
226
+ surfaces: { type: "string", description: "Comma-separated subset of: seo, out, paid, auto, analytics, ai. Default: all." },
227
+ company: { type: "string", description: "One line about the company/product" },
228
+ bottleneck: { type: "string", description: "The bottleneck you are hiring against" }
229
+ },
230
+ additionalProperties: false }
110
231
  }
111
232
  ];
112
233
 
113
234
  function callTool(name, args) {
114
235
  args = args || {};
115
236
  if (name === "list_sections") {
116
- return headingList().map((h) => `${"#".repeat(h.level)} ${h.title}`).join("\n");
237
+ return headingList().map((h) => "#".repeat(h.level) + " " + h.title).join("\n");
117
238
  }
118
239
  if (name === "read_section") {
119
240
  const s = readSection(args.section);
120
- if (!s) return `No section matching "${args.section}". Call list_sections for the index.`;
241
+ if (!s) return 'No section matching "' + args.section + '". Call list_sections for the index.';
121
242
  return s.text;
122
243
  }
123
244
  if (name === "sample_size") {
@@ -140,6 +261,12 @@ function callTool(name, args) {
140
261
  out.volume_in_test = n;
141
262
  return JSON.stringify(out, null, 2);
142
263
  }
264
+ if (name === "aeo_check") {
265
+ return JSON.stringify(aeoCheck(args.checked), null, 2);
266
+ }
267
+ if (name === "jd") {
268
+ return jdText(args);
269
+ }
143
270
  throw new Error("Unknown tool: " + name);
144
271
  }
145
272
 
@@ -154,7 +281,7 @@ function handle(msg) {
154
281
  const { id, method, params } = msg;
155
282
  if (method === "initialize") {
156
283
  return reply(id, { protocolVersion: PROTOCOL, capabilities: { tools: {} }, serverInfo: SERVER,
157
- instructions: "Marketing decision rules as tools. Start with list_sections, then read_section. Use sample_size and kill_rule before spending on a test." });
284
+ instructions: "Marketing decision rules as tools. Start with list_sections, then read_section. Use sample_size and kill_rule before spending on a test, aeo_check to audit whether AI answers can quote a site, and jd to write the role down." });
158
285
  }
159
286
  if (method === "notifications/initialized" || (method || "").startsWith("notifications/")) return;
160
287
  if (method === "ping") return reply(id, {});
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "marketing-mindset-mcp",
3
- "version": "1.0.0",
4
- "description": "MCP server for the open marketing-mindset skill: test-size math, kill rules and the section index as callable tools for any MCP client (Claude Code, Codex, Hermes, Cursor).",
3
+ "version": "1.0.2",
4
+ "description": "MCP server for the open marketing-mindset skill: test-size math, kill rules, AEO agent-readiness audit and a marketing-engineer JD generator as callable tools for any MCP client (Claude Code, Codex, Hermes, Cursor).",
5
5
  "keywords": [
6
+ "ab-testing",
7
+ "aeo",
8
+ "agent-skill",
9
+ "answer-engine-optimization",
10
+ "growth",
11
+ "job-description",
12
+ "marketing",
6
13
  "mcp",
7
- "model-context-protocol",
8
14
  "mcp-server",
9
- "marketing",
10
- "agent-skill",
11
- "sample-size",
12
- "ab-testing",
13
- "growth"
15
+ "model-context-protocol",
16
+ "sample-size"
14
17
  ],
15
18
  "license": "MIT",
16
19
  "author": "Axel Freeman",