quizfunnel-mcp 1.0.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.
Files changed (2) hide show
  1. package/build/index.js +192 -0
  2. package/package.json +31 -0
package/build/index.js ADDED
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ const API_KEY = process.env.QUIZFUNNEL_API_KEY;
6
+ const BASE_URL = (process.env.QUIZFUNNEL_APP_URL || 'https://quizfunnel-dusky.vercel.app').replace(/\/$/, '');
7
+ if (!API_KEY) {
8
+ console.error('QuizFunnel MCP: QUIZFUNNEL_API_KEY manquant.');
9
+ console.error('Générez une clé dans QuizFunnel → Comptes Plateformes → Clés API (MCP)');
10
+ process.exit(1);
11
+ }
12
+ // ─── HTTP helper ────────────────────────────────────────────────────────────
13
+ async function api(method, path, body) {
14
+ const res = await fetch(`${BASE_URL}/api/v1${path}`, {
15
+ method,
16
+ headers: {
17
+ 'Authorization': `Bearer ${API_KEY}`,
18
+ 'Content-Type': 'application/json',
19
+ },
20
+ body: body !== undefined ? JSON.stringify(body) : undefined,
21
+ });
22
+ const data = await res.json();
23
+ if (!res.ok)
24
+ throw new Error(data.error || `HTTP ${res.status}`);
25
+ return data;
26
+ }
27
+ function ok(text) {
28
+ return { content: [{ type: 'text', text }] };
29
+ }
30
+ function json(data) {
31
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
32
+ }
33
+ // ─── server ─────────────────────────────────────────────────────────────────
34
+ const server = new McpServer({ name: 'quizfunnel', version: '2.0.0' });
35
+ server.registerTool('list_quizzes', {
36
+ description: 'Liste tous tes quiz avec statut et URLs.',
37
+ inputSchema: {},
38
+ }, async () => json(await api('GET', '/quizzes')));
39
+ server.registerTool('get_quiz', {
40
+ description: 'Récupère la structure complète d\'un quiz : étapes, options, style.',
41
+ inputSchema: { quiz_id: z.string().describe('ID du quiz') },
42
+ }, async ({ quiz_id }) => json(await api('GET', `/quizzes/${quiz_id}`)));
43
+ server.registerTool('create_quiz', {
44
+ description: 'Crée un nouveau quiz vide.',
45
+ inputSchema: { title: z.string().describe('Titre du quiz') },
46
+ }, async ({ title }) => {
47
+ const data = await api('POST', '/quizzes', { title });
48
+ return ok(`✅ Quiz créé — ID: ${data.id}\nBuilder: ${BASE_URL}/quizzes/${data.id}/builder`);
49
+ });
50
+ server.registerTool('update_quiz_title', {
51
+ description: 'Renomme un quiz.',
52
+ inputSchema: {
53
+ quiz_id: z.string(),
54
+ title: z.string().describe('Nouveau titre'),
55
+ },
56
+ }, async ({ quiz_id, title }) => {
57
+ await api('PATCH', `/quizzes/${quiz_id}`, { title });
58
+ return ok(`✅ Quiz renommé en "${title}"`);
59
+ });
60
+ server.registerTool('publish_quiz', {
61
+ description: 'Publie ou dépublie un quiz.',
62
+ inputSchema: {
63
+ quiz_id: z.string(),
64
+ publish: z.boolean().describe('true=publier, false=brouillon'),
65
+ },
66
+ }, async ({ quiz_id, publish }) => {
67
+ const data = await api('POST', `/quizzes/${quiz_id}/publish`, { publish });
68
+ return ok(publish ? `✅ Quiz publié — ${BASE_URL}/q/${quiz_id}` : `✅ Quiz dépublié (brouillon)`);
69
+ });
70
+ server.registerTool('delete_quiz', {
71
+ description: 'Supprime définitivement un quiz.',
72
+ inputSchema: {
73
+ quiz_id: z.string(),
74
+ confirm: z.boolean().describe('Doit être true'),
75
+ },
76
+ }, async ({ quiz_id, confirm }) => {
77
+ if (!confirm)
78
+ return ok('Annulé. Passe confirm: true.');
79
+ await api('DELETE', `/quizzes/${quiz_id}`);
80
+ return ok(`✅ Quiz supprimé.`);
81
+ });
82
+ server.registerTool('add_questions', {
83
+ description: 'Ajoute une ou plusieurs questions avec leurs options de réponse.',
84
+ inputSchema: {
85
+ quiz_id: z.string(),
86
+ questions: z.array(z.object({
87
+ title: z.string().describe('Texte de la question'),
88
+ question_type: z.enum(['single_choice', 'multiple_choice']),
89
+ options: z.array(z.object({
90
+ label: z.string(),
91
+ image_url: z.string().optional(),
92
+ tag: z.string().optional().describe('Ex: qualifie, chaud, debutant'),
93
+ })).min(2),
94
+ })),
95
+ },
96
+ }, async ({ quiz_id, questions }) => {
97
+ const created = await api('POST', `/quizzes/${quiz_id}/steps`, questions);
98
+ const lines = questions.map((q) => ` • "${q.title}" (${q.options.length} options)`);
99
+ return ok(`✅ ${questions.length} question(s) ajoutée(s) :\n${lines.join('\n')}`);
100
+ });
101
+ server.registerTool('add_trust_card', {
102
+ description: 'Ajoute une carte de confiance / preuve sociale.',
103
+ inputSchema: {
104
+ quiz_id: z.string(),
105
+ card_title: z.string().describe('Ex: "97% de nos clients sont satisfaits"'),
106
+ description: z.string().optional(),
107
+ },
108
+ }, async ({ quiz_id, card_title, description }) => {
109
+ await api('POST', `/quizzes/${quiz_id}/steps`, [{ type: 'trust_card', card_title, description }]);
110
+ return ok(`✅ Carte ajoutée : "${card_title}"`);
111
+ });
112
+ server.registerTool('update_question', {
113
+ description: 'Modifie titre et/ou options d\'une question existante.',
114
+ inputSchema: {
115
+ quiz_id: z.string(),
116
+ step_id: z.string().describe('ID de l\'étape (obtenu via get_quiz)'),
117
+ title: z.string().optional(),
118
+ question_type: z.enum(['single_choice', 'multiple_choice']).optional(),
119
+ options: z.array(z.object({
120
+ label: z.string(),
121
+ image_url: z.string().optional(),
122
+ tag: z.string().optional(),
123
+ })).optional().describe('Remplace toutes les options si fourni'),
124
+ },
125
+ }, async ({ quiz_id, step_id, title, question_type, options }) => {
126
+ await api('PATCH', `/quizzes/${quiz_id}/steps/${step_id}`, { title, question_type, options });
127
+ return ok(`✅ Question mise à jour.`);
128
+ });
129
+ server.registerTool('delete_step', {
130
+ description: 'Supprime une étape (question ou carte).',
131
+ inputSchema: {
132
+ quiz_id: z.string(),
133
+ step_id: z.string().describe('ID de l\'étape'),
134
+ },
135
+ }, async ({ quiz_id, step_id }) => {
136
+ await api('DELETE', `/quizzes/${quiz_id}/steps/${step_id}`);
137
+ return ok(`✅ Étape supprimée.`);
138
+ });
139
+ server.registerTool('clear_quiz', {
140
+ description: 'Supprime toutes les étapes d\'un quiz.',
141
+ inputSchema: {
142
+ quiz_id: z.string(),
143
+ confirm: z.boolean(),
144
+ },
145
+ }, async ({ quiz_id, confirm }) => {
146
+ if (!confirm)
147
+ return ok('Annulé. Passe confirm: true.');
148
+ const quiz = await api('GET', `/quizzes/${quiz_id}`);
149
+ const count = quiz.steps?.length || 0;
150
+ for (const step of quiz.steps || []) {
151
+ await api('DELETE', `/quizzes/${quiz_id}/steps/${step.id}`);
152
+ }
153
+ return ok(`✅ Quiz vidé — ${count} étape(s) supprimée(s).`);
154
+ });
155
+ server.registerTool('update_style', {
156
+ description: 'Modifie le style visuel : couleurs, fond, police, bouton.',
157
+ inputSchema: {
158
+ quiz_id: z.string(),
159
+ background_type: z.enum(['solid', 'gradient']).optional(),
160
+ background_color: z.string().optional().describe('Hex, ex: "#ffffff"'),
161
+ gradient_from: z.string().optional(),
162
+ gradient_to: z.string().optional(),
163
+ button_color: z.string().optional().describe('Hex, ex: "#E53935"'),
164
+ button_text_color: z.string().optional(),
165
+ progress_bar_color: z.string().optional(),
166
+ font_family: z.enum(['Inter', 'Poppins', 'Roboto', 'Montserrat', 'Playfair Display']).optional(),
167
+ border_radius: z.number().min(0).max(32).optional(),
168
+ shadow_enabled: z.boolean().optional(),
169
+ },
170
+ }, async ({ quiz_id, ...style }) => {
171
+ const res = await api('PATCH', `/quizzes/${quiz_id}/style`, style);
172
+ const parts = (res.updated || []).map((k) => ` • ${k}: ${style[k]}`);
173
+ return ok(`🎨 Style mis à jour :\n${parts.join('\n')}`);
174
+ });
175
+ server.registerTool('get_leads', {
176
+ description: 'Récupère les leads d\'un quiz.',
177
+ inputSchema: {
178
+ quiz_id: z.string(),
179
+ limit: z.number().min(1).max(100).default(20),
180
+ },
181
+ }, async ({ quiz_id, limit }) => json(await api('GET', `/quizzes/${quiz_id}/leads?limit=${limit}`)));
182
+ server.registerTool('get_analytics', {
183
+ description: 'Stats du funnel : vues, complétions, drop-off par étape.',
184
+ inputSchema: { quiz_id: z.string() },
185
+ }, async ({ quiz_id }) => json(await api('GET', `/quizzes/${quiz_id}/analytics`)));
186
+ // ─── start ───────────────────────────────────────────────────────────────────
187
+ async function main() {
188
+ const transport = new StdioServerTransport();
189
+ await server.connect(transport);
190
+ console.error('QuizFunnel MCP v2 running — connected to', BASE_URL);
191
+ }
192
+ main().catch((err) => { console.error('Fatal:', err); process.exit(1); });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "quizfunnel-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for QuizFunnel — build quiz funnels by talking to Claude",
5
+ "type": "module",
6
+ "main": "build/index.js",
7
+ "bin": {
8
+ "quizfunnel-mcp": "build/index.js"
9
+ },
10
+ "files": [
11
+ "build"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc && chmod 755 build/index.js",
15
+ "prepublishOnly": "npm run build",
16
+ "start": "node build/index.js"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.12.0",
20
+ "zod": "^3.25.42"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22",
24
+ "typescript": "^5"
25
+ },
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "keywords": ["mcp", "claude", "quizfunnel", "quiz", "funnel", "marketing"],
30
+ "license": "MIT"
31
+ }