@coppica/persuasion-mcp 1.0.2 → 1.0.4
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/README.md +1 -1
- package/data/catalog.json +1 -1
- package/data/categories.json +1 -1
- package/dist/annotations.d.ts +15 -0
- package/dist/annotations.js +16 -0
- package/dist/format.d.ts +4 -0
- package/dist/format.js +7 -1
- package/dist/server.js +3 -2
- package/dist/tools/catalog.js +5 -6
- package/dist/tools/craft.js +3 -4
- package/dist/tools/diagnose.js +2 -2
- package/dist/tools/plan.js +4 -4
- package/dist/tools/testing.js +3 -4
- package/dist/tools/why.js +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The title and annotations every tool here shares. Each tool only reads the bundled catalog or does
|
|
4
|
+
* arithmetic: it writes, deletes and sends nothing, gives the same answer to the same input, and
|
|
5
|
+
* reaches nothing outside the catalog.
|
|
6
|
+
*
|
|
7
|
+
* The hints are spelled out on purpose. Claude's directory wants a title plus readOnlyHint or
|
|
8
|
+
* destructiveHint on every tool, and OpenAI's plugin review wants readOnlyHint, destructiveHint and
|
|
9
|
+
* openWorldHint as explicit booleans. The title goes in the annotations too, for clients on the
|
|
10
|
+
* 2025-03-26 protocol, which read it from there.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readOnlyTool(title: string): {
|
|
13
|
+
title: string;
|
|
14
|
+
annotations: ToolAnnotations;
|
|
15
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The title and annotations every tool here shares. Each tool only reads the bundled catalog or does
|
|
3
|
+
* arithmetic: it writes, deletes and sends nothing, gives the same answer to the same input, and
|
|
4
|
+
* reaches nothing outside the catalog.
|
|
5
|
+
*
|
|
6
|
+
* The hints are spelled out on purpose. Claude's directory wants a title plus readOnlyHint or
|
|
7
|
+
* destructiveHint on every tool, and OpenAI's plugin review wants readOnlyHint, destructiveHint and
|
|
8
|
+
* openWorldHint as explicit booleans. The title goes in the annotations too, for clients on the
|
|
9
|
+
* 2025-03-26 protocol, which read it from there.
|
|
10
|
+
*/
|
|
11
|
+
export function readOnlyTool(title) {
|
|
12
|
+
return {
|
|
13
|
+
title,
|
|
14
|
+
annotations: { title, readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
15
|
+
};
|
|
16
|
+
}
|
package/dist/format.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Technique, Example } from './data.js';
|
|
2
2
|
export declare const FOOTER: string;
|
|
3
|
+
/** Links a reader follows out of an answer carry utm_source=mcp, so the Taxonomy's analytics can
|
|
4
|
+
* count the visits that arrive through an AI assistant. Citations and the CC BY footer stay clean,
|
|
5
|
+
* because people paste those into their own work. */
|
|
6
|
+
export declare function tracked(url: string): string;
|
|
3
7
|
export declare function text(body: string): {
|
|
4
8
|
content: {
|
|
5
9
|
type: "text";
|
package/dist/format.js
CHANGED
|
@@ -2,6 +2,12 @@ import { tierIn } from './data.js';
|
|
|
2
2
|
import { clipText, innerQuotes } from './voice.js';
|
|
3
3
|
export const FOOTER = '\n\n---\nFrom the Persuasion Taxonomy by Coppica, free to use under CC BY 4.0 at https://taxonomy.coppica.com. ' +
|
|
4
4
|
'When you use a technique, name it and link to it, so your reader can see the real examples behind it.';
|
|
5
|
+
/** Links a reader follows out of an answer carry utm_source=mcp, so the Taxonomy's analytics can
|
|
6
|
+
* count the visits that arrive through an AI assistant. Citations and the CC BY footer stay clean,
|
|
7
|
+
* because people paste those into their own work. */
|
|
8
|
+
export function tracked(url) {
|
|
9
|
+
return `${url}${url.includes('?') ? '&' : '?'}utm_source=mcp`;
|
|
10
|
+
}
|
|
5
11
|
export function text(body) {
|
|
6
12
|
return { content: [{ type: 'text', text: body + FOOTER }] };
|
|
7
13
|
}
|
|
@@ -46,5 +52,5 @@ export function techniqueLine(t, opts = {}) {
|
|
|
46
52
|
}
|
|
47
53
|
if (opts.after)
|
|
48
54
|
line += ` ${opts.after}`;
|
|
49
|
-
return `${line} ${t.url}`;
|
|
55
|
+
return `${line} ${tracked(t.url)}`;
|
|
50
56
|
}
|
package/dist/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
4
4
|
import { ROLE_NAMES, ROLES } from './roles.js';
|
|
5
5
|
import { TECHNIQUES, CATALOG_META } from './data.js';
|
|
6
6
|
import { cap } from './voice.js';
|
|
7
|
+
import { tracked } from './format.js';
|
|
7
8
|
import { registerPlanTool } from './tools/plan.js';
|
|
8
9
|
import { registerDiagnoseTool } from './tools/diagnose.js';
|
|
9
10
|
import { registerWhyTool } from './tools/why.js';
|
|
@@ -36,14 +37,14 @@ function nineQuestionsDoc() {
|
|
|
36
37
|
const lines = [
|
|
37
38
|
'# The nine reader questions',
|
|
38
39
|
'',
|
|
39
|
-
|
|
40
|
+
`From The 18 Theses, Thesis VIII: ${tracked('https://taxonomy.coppica.com/the-18-theses')}`,
|
|
40
41
|
'',
|
|
41
42
|
'Everyone who reads an ad, a page or an email is silently asking nine questions. Copy persuades when it answers the ones its goal needs, truthfully, and the answers hold together.',
|
|
42
43
|
'',
|
|
43
44
|
];
|
|
44
45
|
for (const r of ROLE_NAMES) {
|
|
45
46
|
const i = ROLES[r];
|
|
46
|
-
lines.push(`${i.num}. **${i.question}** (${r}) This one is about ${i.gloss}. Leave it unanswered and here is what happens: "${i.ifMissing}" https://taxonomy.coppica.com/${r.toLowerCase()}`);
|
|
47
|
+
lines.push(`${i.num}. **${i.question}** (${r}) This one is about ${i.gloss}. Leave it unanswered and here is what happens: "${i.ifMissing}" ${tracked(`https://taxonomy.coppica.com/${r.toLowerCase()}`)}`);
|
|
47
48
|
}
|
|
48
49
|
lines.push('', 'Real persuasion happens when every question a piece needs is answered, in the same person, at the same time, and the answers agree with each other. ' +
|
|
49
50
|
'When one answer contradicts another, the reader feels that something is off, and they discount everything.');
|
package/dist/tools/catalog.js
CHANGED
|
@@ -2,11 +2,12 @@ import { z } from 'zod';
|
|
|
2
2
|
import { ROLES, roleLabel } from '../roles.js';
|
|
3
3
|
import { searchTechniques, lookupTechnique, resolveCategory, TECHNIQUES } from '../data.js';
|
|
4
4
|
import { formatSchema, categorySchema, roleSchema } from '../schemas.js';
|
|
5
|
-
import { text, errorText, techniqueLine, usageSentence, exampleSource } from '../format.js';
|
|
5
|
+
import { text, errorText, techniqueLine, usageSentence, exampleSource, tracked } from '../format.js';
|
|
6
6
|
import { joinList, clipText, innerQuotes } from '../voice.js';
|
|
7
|
+
import { readOnlyTool } from '../annotations.js';
|
|
7
8
|
export function registerCatalogTools(server) {
|
|
8
9
|
server.registerTool('find_persuasion_techniques', {
|
|
9
|
-
|
|
10
|
+
...readOnlyTool('Find persuasion techniques'),
|
|
10
11
|
description: `Search the Persuasion Taxonomy, ${TECHNIQUES.length} named persuasion techniques documented from real advertising and organized by the nine reader questions they answer. ` +
|
|
11
12
|
'Describe what you want the copy to do, like build trust without testimonials, create urgency without fake scarcity, handle a price objection, make an opening less generic, ' +
|
|
12
13
|
'prove a claim or reframe a problem. It finds the techniques that do it. Each one comes with its name and ID, a one-line definition, a real example, ' +
|
|
@@ -19,7 +20,6 @@ export function registerCatalogTools(server) {
|
|
|
19
20
|
category: categorySchema.optional(),
|
|
20
21
|
limit: z.number().int().min(1).max(20).default(8),
|
|
21
22
|
},
|
|
22
|
-
annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
|
|
23
23
|
}, async ({ query, question, format, category, limit }) => {
|
|
24
24
|
const cat = resolveCategory(category);
|
|
25
25
|
const role = question ?? null;
|
|
@@ -36,7 +36,7 @@ export function registerCatalogTools(server) {
|
|
|
36
36
|
return text(out.join('\n'));
|
|
37
37
|
});
|
|
38
38
|
server.registerTool('get_persuasion_technique', {
|
|
39
|
-
|
|
39
|
+
...readOnlyTool('Get a persuasion technique'),
|
|
40
40
|
description: 'Look up one persuasion technique by its ID (like PT-PRV-10000), its slug or its name. You get the full definition, why it works, when it lands and when it backfires, ' +
|
|
41
41
|
'real examples from advertising, related and opposite techniques, and a link you can cite. ' +
|
|
42
42
|
'Use it before you apply or recommend a technique so you get it right, and whenever someone asks what a tactic is called or how it works.',
|
|
@@ -44,7 +44,6 @@ export function registerCatalogTools(server) {
|
|
|
44
44
|
id: z.string().min(2).max(160).describe('The PT-ID, the slug or the exact name of the technique.'),
|
|
45
45
|
category: categorySchema.optional(),
|
|
46
46
|
},
|
|
47
|
-
annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
|
|
48
47
|
}, async ({ id, category }) => {
|
|
49
48
|
const t = lookupTechnique(id);
|
|
50
49
|
if (!t) {
|
|
@@ -55,7 +54,7 @@ export function registerCatalogTools(server) {
|
|
|
55
54
|
const cat = resolveCategory(category);
|
|
56
55
|
const out = [];
|
|
57
56
|
out.push(`# ${t.name} (${t.pt_id})`);
|
|
58
|
-
out.push(`It answers ${roleLabel(t.role)}, which is about ${ROLES[t.role].gloss}. ${usageSentence(t, cat)} ${t.url}`);
|
|
57
|
+
out.push(`It answers ${roleLabel(t.role)}, which is about ${ROLES[t.role].gloss}. ${usageSentence(t, cat)} ${tracked(t.url)}`);
|
|
59
58
|
out.push('');
|
|
60
59
|
// Catalog fields: whole sentences only (the export cuts long fields), and condition lists read as sentences.
|
|
61
60
|
const prose = (s) => clipText(s.replace(/;\s+(?=[A-Z])/g, '. '), 4000);
|
package/dist/tools/craft.js
CHANGED
|
@@ -5,6 +5,7 @@ import { scoreClaims } from '../scorers/claim-framework.js';
|
|
|
5
5
|
import { detectAISmell } from '../scorers/hook-framework.js';
|
|
6
6
|
import { SLOTS, SLOT_INFO, checkHook, compareHooks, describeCheck, questionsNeeded, article, VERDICT_LABEL, EVIDENCE_SOURCE } from '../hookcheck/index.js';
|
|
7
7
|
import { claimGateSentence, smellSentences, joinList, plural, endSentence } from '../voice.js';
|
|
8
|
+
import { readOnlyTool } from '../annotations.js';
|
|
8
9
|
/** Unproven superlatives the claim scorer can read as "specific" (e.g. "#1"). */
|
|
9
10
|
const SUPERLATIVE = /(#\s?1\b|\bnumber one\b|\bthe best\b|\bbest[- ]in[- ]class\b|\bleading\b|\bworld'?s (best|first|most)\b|\btop[- ]rated\b|\bunmatched\b|\brevolutionary\b|\bgame[- ]chang)/i;
|
|
10
11
|
const BACKED = /\b(according to|rated by|ranked by|verified|audited|source:|by g2|by capterra|survey|study|since \d{4}|\d[\d,.]*\s?(%|customers|users|reviews|stars))/i;
|
|
@@ -18,7 +19,7 @@ const briefSchema = z.object({
|
|
|
18
19
|
}).describe('The brief. The swap test needs it to tell whether a competitor could send the line unchanged, and without it no line can pass.');
|
|
19
20
|
export function registerCraftTools(server) {
|
|
20
21
|
server.registerTool('check_headlines', {
|
|
21
|
-
|
|
22
|
+
...readOnlyTool('Check and compare headlines, hooks, and subject lines'),
|
|
22
23
|
description: 'Check headlines, email subject lines, ad openings, video hooks, social post openings and first lines, and compare options against each other. ' +
|
|
23
24
|
'For each line it tells you whether the line does its job in that spot, meaning which of the nine reader questions it answers, and whether a competitor could send it unchanged. ' +
|
|
24
25
|
'It also shows exactly what a reader sees before the cutoff, flags anything that fails outright, and brings in evidence from thousands of real headline tests. ' +
|
|
@@ -38,7 +39,6 @@ export function registerCraftTools(server) {
|
|
|
38
39
|
follows: z.string().max(600).optional().describe('What the reader saw just before this line, like the ad before a landing page headline, or the headline or subject before a first line.'),
|
|
39
40
|
awareness_level: awarenessSchema.optional(),
|
|
40
41
|
},
|
|
41
|
-
annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
|
|
42
42
|
}, async ({ slot, lines, brief, follows, awareness_level }) => {
|
|
43
43
|
const info = SLOT_INFO[slot];
|
|
44
44
|
const names = lines.map((_, i) => (lines.length > 1 ? String.fromCharCode(65 + i) : 'This line'));
|
|
@@ -75,7 +75,7 @@ export function registerCraftTools(server) {
|
|
|
75
75
|
return text(out.join('\n'));
|
|
76
76
|
});
|
|
77
77
|
server.registerTool('check_marketing_claims', {
|
|
78
|
-
|
|
78
|
+
...readOnlyTool('Check marketing claims and CTAs'),
|
|
79
79
|
description: 'Check whether marketing claims and calls to action are specific enough to believe. It catches the vague superlative, like "the best" or "#1", that readers discount on sight. ' +
|
|
80
80
|
'It catches promises with no proof behind them and results with no mechanism, and it flags the generic buttons everyone uses, like "Learn more" and "Get started". ' +
|
|
81
81
|
'Then it tells you how to fix each one. Use it when you write or review claims like "results in days", "trusted by thousands" or "the #1 tool", and for any button text. ' +
|
|
@@ -87,7 +87,6 @@ export function registerCraftTools(server) {
|
|
|
87
87
|
context: z.string().max(1500).optional().describe('For a claim in the body, the paragraph around it, so a mechanism named nearby still counts.'),
|
|
88
88
|
})).min(1).max(30),
|
|
89
89
|
},
|
|
90
|
-
annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
|
|
91
90
|
}, async ({ claims }) => {
|
|
92
91
|
const out = [`# ${plural(claims.length, 'claim')}, checked`];
|
|
93
92
|
// Headline claims go through the hook check's facts; the old headline scorer did not predict winners.
|
package/dist/tools/diagnose.js
CHANGED
|
@@ -6,6 +6,7 @@ import { text, techniqueLine } from '../format.js';
|
|
|
6
6
|
import { scoreFlow } from '../scorers/flow-framework.js';
|
|
7
7
|
import { checkHook, describeCheck, VERDICT_LABEL } from '../hookcheck/index.js';
|
|
8
8
|
import { flowGateSentence, joinList, plural } from '../voice.js';
|
|
9
|
+
import { readOnlyTool } from '../annotations.js';
|
|
9
10
|
export const OPENING_SLOT = {
|
|
10
11
|
ad: 'ad_opening', landing_page: 'landing_page_headline', sales_letter: 'sales_letter_headline', email: 'first_line',
|
|
11
12
|
email_subject: 'email_subject', social_post: 'social_post_open', video_script: 'video_hook', headline: 'landing_page_headline',
|
|
@@ -45,7 +46,7 @@ const normText = (s) => s.toLowerCase().replace(/[‘’]/g, "'").replace(/[“
|
|
|
45
46
|
const STATUS_RANK = { missing: 0, partial: 1, clear: 2 };
|
|
46
47
|
export function registerDiagnoseTool(server) {
|
|
47
48
|
server.registerTool('diagnose_marketing_copy', {
|
|
48
|
-
|
|
49
|
+
...readOnlyTool('Diagnose marketing copy (roast / review)'),
|
|
49
50
|
description: "Review, critique or roast any piece of marketing copy, whether it's an ad, a landing page, an email, a sales page, a social post or a script, and find out why it might not persuade. " +
|
|
50
51
|
'Before you call it, read the copy and label each line with the one reader question it mainly answers, quoting the words exactly. Those labels go in the moves array. ' +
|
|
51
52
|
'You get back which of the nine questions the copy answers for its goal, which it answers weakly and which it skips, along with the biggest gap and what readers do when it is missing. ' +
|
|
@@ -73,7 +74,6 @@ export function registerDiagnoseTool(server) {
|
|
|
73
74
|
proof: z.string().max(400).optional(),
|
|
74
75
|
}).optional().describe('The brief, if you have it: the brand, product, offer, audience and proof. With it, the opening line gets the swap test, which asks whether a competitor could send it unchanged.'),
|
|
75
76
|
},
|
|
76
|
-
annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
|
|
77
77
|
}, async ({ copy, goal, format, moves, awareness_level, category, headline, persuasion_mode, brief }) => {
|
|
78
78
|
const cat = resolveCategory(category);
|
|
79
79
|
const expected = expectedRoles(goal);
|
package/dist/tools/plan.js
CHANGED
|
@@ -2,12 +2,13 @@ import { z } from 'zod';
|
|
|
2
2
|
import { ROLE_NAMES, ROLES, GOAL_REQUIREMENTS, GOAL_NOUN, GOAL_CLAUSE, FORMAT_LABEL, AWARENESS_WEIGHT, COHERENCE_CHECKS, expectedRoles, roleLabel } from '../roles.js';
|
|
3
3
|
import { pickMoves, resolveCategory } from '../data.js';
|
|
4
4
|
import { goalSchema, formatSchema, awarenessSchema, categorySchema, modeSchema } from '../schemas.js';
|
|
5
|
-
import { text, techniqueLine } from '../format.js';
|
|
5
|
+
import { text, techniqueLine, tracked } from '../format.js';
|
|
6
6
|
import { cap, joinList } from '../voice.js';
|
|
7
|
+
import { readOnlyTool } from '../annotations.js';
|
|
7
8
|
const NUM = ['no', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
|
|
8
9
|
export function registerPlanTool(server) {
|
|
9
10
|
server.registerTool('plan_marketing_copy', {
|
|
10
|
-
|
|
11
|
+
...readOnlyTool('Plan marketing copy (the nine reader questions)'),
|
|
11
12
|
description: "Plan any piece of marketing copy before you write it, whether it's an ad, a landing page, a sales page or sales letter, an email, a subject line, a social post, a headline or a video script. " +
|
|
12
13
|
'Give it the goal, the reader and the offer, and it tells you which of the nine questions every reader silently asks this piece has to answer, which ones deserve the most words for this reader, ' +
|
|
13
14
|
'and a few ways to answer each one, drawn from real advertising and linked to the Persuasion Taxonomy. One option is always the established move and the rest are less expected, ' +
|
|
@@ -23,7 +24,6 @@ export function registerPlanTool(server) {
|
|
|
23
24
|
persuasion_mode: modeSchema,
|
|
24
25
|
options_per_question: z.number().int().min(2).max(6).default(4).describe('How many ways to answer each question you want to see. The default is 4.'),
|
|
25
26
|
},
|
|
26
|
-
annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
|
|
27
27
|
}, async ({ goal, format, audience, offer, awareness_level, category, persuasion_mode, options_per_question }) => {
|
|
28
28
|
const cat = resolveCategory(category);
|
|
29
29
|
const awareness = awareness_level ?? 'problem_aware';
|
|
@@ -78,7 +78,7 @@ export function registerPlanTool(server) {
|
|
|
78
78
|
lines.push(`${cap(GOAL_NOUN[goal])} doesn't strictly need these, but one well-placed line can still help.`);
|
|
79
79
|
for (const role of optional) {
|
|
80
80
|
const [p] = pickMoves(role, { n: 1, format, category: cat, seed, context, mode, exclude: used });
|
|
81
|
-
lines.push(`- ${roleLabel(role)}${p ? ` One way in is **${p.t.name}** (${p.t.pt_id}). ${p.t.url}` : ''}`);
|
|
81
|
+
lines.push(`- ${roleLabel(role)}${p ? ` One way in is **${p.t.name}** (${p.t.pt_id}). ${tracked(p.t.url)}` : ''}`);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
const checks = COHERENCE_CHECKS.filter((c) => expected.has(c.roles[0]) && expected.has(c.roles[1])).slice(0, 4);
|
package/dist/tools/testing.js
CHANGED
|
@@ -4,13 +4,14 @@ import { roleSchema } from '../schemas.js';
|
|
|
4
4
|
import { text, errorText } from '../format.js';
|
|
5
5
|
import { sampleSizePerArm, compare, srmPValue } from '../stats.js';
|
|
6
6
|
import { joinList, plural } from '../voice.js';
|
|
7
|
+
import { readOnlyTool } from '../annotations.js';
|
|
7
8
|
const pct = (v, d = 2) => `${(v * 100).toFixed(d)}%`;
|
|
8
9
|
const points = (v) => `${(v * 100).toFixed(2)}`;
|
|
9
10
|
const fmt = (n) => n.toLocaleString('en-US');
|
|
10
11
|
const pValue = (p) => (p < 0.0001 ? 'below 0.0001' : p.toFixed(4));
|
|
11
12
|
export function registerTestingTools(server) {
|
|
12
13
|
server.registerTool('plan_ab_test', {
|
|
13
|
-
|
|
14
|
+
...readOnlyTool('Plan an A/B test'),
|
|
14
15
|
description: 'Plan an A/B test for marketing copy. Give it the current conversion rate, the smallest lift worth detecting and the daily traffic, ' +
|
|
15
16
|
'and it tells you how many visitors each version needs and how many days to run. It also frames the test as two different answers to the same reader question, ' +
|
|
16
17
|
'so the result teaches you something you can reuse, and not just that "B won". ' +
|
|
@@ -26,7 +27,6 @@ export function registerTestingTools(server) {
|
|
|
26
27
|
version_a: z.string().max(600).optional().describe('How version A answers it.'),
|
|
27
28
|
version_b: z.string().max(600).optional().describe('How version B answers it.'),
|
|
28
29
|
},
|
|
29
|
-
annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
|
|
30
30
|
}, async (a) => {
|
|
31
31
|
const base = a.baseline_conversion_rate_percent / 100;
|
|
32
32
|
const lift = a.minimum_detectable_lift_percent / 100;
|
|
@@ -67,7 +67,7 @@ export function registerTestingTools(server) {
|
|
|
67
67
|
return text(out.join('\n'));
|
|
68
68
|
});
|
|
69
69
|
server.registerTool('read_ab_test_result', {
|
|
70
|
-
|
|
70
|
+
...readOnlyTool('Read an A/B test result'),
|
|
71
71
|
description: "Read the result of an A/B test. You get each version's conversion rate and lift, whether the difference is statistically significant, the confidence interval, " +
|
|
72
72
|
'and the chance each version really beats the original. It also warns you about the things that make a test result lie. Those include too few conversions, ' +
|
|
73
73
|
"traffic that didn't split the way it should (a sample ratio mismatch), stopping the moment it looked good, and testing too many versions at once. " +
|
|
@@ -82,7 +82,6 @@ export function registerTestingTools(server) {
|
|
|
82
82
|
confidence: z.number().min(0.8).max(0.999).default(0.95),
|
|
83
83
|
question: roleSchema.optional().describe('The reader question the versions answered in different ways.'),
|
|
84
84
|
},
|
|
85
|
-
annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
|
|
86
85
|
}, async ({ versions, intended_split, confidence, question }) => {
|
|
87
86
|
if (versions.some((v) => v.conversions > v.visitors))
|
|
88
87
|
return errorText('One of the versions has more conversions than visitors, so check the numbers.');
|
package/dist/tools/why.js
CHANGED
|
@@ -3,9 +3,10 @@ import { ROLES, SYMPTOMS, SYMPTOM_MAP, roleLabel } from '../roles.js';
|
|
|
3
3
|
import { pickMoves, resolveCategory } from '../data.js';
|
|
4
4
|
import { formatSchema, categorySchema, modeSchema } from '../schemas.js';
|
|
5
5
|
import { text, techniqueLine } from '../format.js';
|
|
6
|
+
import { readOnlyTool } from '../annotations.js';
|
|
6
7
|
export function registerWhyTool(server) {
|
|
7
8
|
server.registerTool('explain_why_not_converting', {
|
|
8
|
-
|
|
9
|
+
...readOnlyTool('Explain why marketing is not converting'),
|
|
9
10
|
description: "Work out why an ad, a landing page, an email or a whole funnel isn't converting. Start from what you can see. " +
|
|
10
11
|
"Maybe people don't click (a low CTR), or they click and bounce, or they read and don't buy. Maybe they abandon the checkout or the form, the leads never buy, " +
|
|
11
12
|
"customers buy once and leave, or emails don't get opened or get opened and nobody clicks. " +
|
|
@@ -21,7 +22,6 @@ export function registerWhyTool(server) {
|
|
|
21
22
|
category: categorySchema.optional(),
|
|
22
23
|
persuasion_mode: modeSchema,
|
|
23
24
|
},
|
|
24
|
-
annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
|
|
25
25
|
}, async ({ symptom, format, details, category, persuasion_mode }) => {
|
|
26
26
|
const info = SYMPTOM_MAP[symptom];
|
|
27
27
|
const cat = resolveCategory(category);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coppica/persuasion-mcp",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "A free MCP server that helps any AI write and check marketing copy: the nine questions every reader asks,
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "A free MCP server that helps any AI write and check marketing copy: the nine questions every reader asks, 767 persuasion techniques from real ads, copy diagnostics and A/B test math. No API key, no AI calls.",
|
|
5
5
|
"mcpName": "com.coppica/persuasion-taxonomy",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|