@connect-xlab/mcp-server 0.1.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 (72) hide show
  1. package/dist/config.js +18 -0
  2. package/dist/http.js +30 -0
  3. package/dist/index.js +178 -0
  4. package/dist/tools/addComment.js +31 -0
  5. package/dist/tools/approveMember.js +29 -0
  6. package/dist/tools/banMember.js +26 -0
  7. package/dist/tools/createCategory.js +36 -0
  8. package/dist/tools/createClass.js +37 -0
  9. package/dist/tools/createEvent.js +39 -0
  10. package/dist/tools/createLabLink.js +28 -0
  11. package/dist/tools/createNewsletterEdition.js +38 -0
  12. package/dist/tools/createPodcastEpisode.js +38 -0
  13. package/dist/tools/createPodcastShow.js +34 -0
  14. package/dist/tools/createPost.js +41 -0
  15. package/dist/tools/createShopProduct.js +46 -0
  16. package/dist/tools/deleteCategory.js +26 -0
  17. package/dist/tools/deleteClass.js +26 -0
  18. package/dist/tools/deleteEvent.js +26 -0
  19. package/dist/tools/deleteLabLink.js +26 -0
  20. package/dist/tools/deletePost.js +26 -0
  21. package/dist/tools/deleteShopProduct.js +23 -0
  22. package/dist/tools/demoteAdmin.js +26 -0
  23. package/dist/tools/getEarnings.js +28 -0
  24. package/dist/tools/getLabSettings.js +23 -0
  25. package/dist/tools/getShopSales.js +35 -0
  26. package/dist/tools/getStats.js +23 -0
  27. package/dist/tools/kickMember.js +29 -0
  28. package/dist/tools/listAdmins.js +23 -0
  29. package/dist/tools/listCategories.js +29 -0
  30. package/dist/tools/listChannelSubscribers.js +35 -0
  31. package/dist/tools/listClasses.js +23 -0
  32. package/dist/tools/listComments.js +31 -0
  33. package/dist/tools/listEvents.js +33 -0
  34. package/dist/tools/listInvitations.js +23 -0
  35. package/dist/tools/listLabLinks.js +23 -0
  36. package/dist/tools/listLabs.js +32 -0
  37. package/dist/tools/listLevels.js +23 -0
  38. package/dist/tools/listMembers.js +49 -0
  39. package/dist/tools/listNewsletterEditions.js +18 -0
  40. package/dist/tools/listNewsletterSubscribers.js +31 -0
  41. package/dist/tools/listPlugins.js +23 -0
  42. package/dist/tools/listPodcastEpisodes.js +31 -0
  43. package/dist/tools/listPodcastShows.js +18 -0
  44. package/dist/tools/listPodcastSubscribers.js +26 -0
  45. package/dist/tools/listPosts.js +49 -0
  46. package/dist/tools/listShops.js +28 -0
  47. package/dist/tools/newsletterDashboard.js +18 -0
  48. package/dist/tools/pinPost.js +31 -0
  49. package/dist/tools/promoteAdmin.js +26 -0
  50. package/dist/tools/publishPodcastEpisode.js +31 -0
  51. package/dist/tools/rejectMember.js +29 -0
  52. package/dist/tools/revokeInvitation.js +26 -0
  53. package/dist/tools/searchMembers.js +33 -0
  54. package/dist/tools/sendDm.js +35 -0
  55. package/dist/tools/sendInvitation.js +34 -0
  56. package/dist/tools/sendNewsletterEdition.js +23 -0
  57. package/dist/tools/setMetaPixel.js +34 -0
  58. package/dist/tools/setWelcomeDm.js +38 -0
  59. package/dist/tools/togglePlugin.js +27 -0
  60. package/dist/tools/unbanMember.js +26 -0
  61. package/dist/tools/unpublishPodcastEpisode.js +31 -0
  62. package/dist/tools/updateCategory.js +38 -0
  63. package/dist/tools/updateClass.js +42 -0
  64. package/dist/tools/updateEvent.js +44 -0
  65. package/dist/tools/updateLabLink.js +34 -0
  66. package/dist/tools/updateLabSettings.js +46 -0
  67. package/dist/tools/updateLevel.js +34 -0
  68. package/dist/tools/updatePodcastEpisode.js +40 -0
  69. package/dist/tools/updatePost.js +36 -0
  70. package/dist/tools/updateShopProduct.js +46 -0
  71. package/dist/tools/updateSlug.js +29 -0
  72. package/package.json +29 -0
package/dist/config.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Config du serveur MCP. Toutes les valeurs viennent de l'environnement,
3
+ * poussees par Claude Desktop (via env dans claude_desktop_config.json)
4
+ * ou par ton shell si tu lances en dev.
5
+ */
6
+ const requireEnv = (key) => {
7
+ const v = process.env[key];
8
+ if (!v || v.trim() === '') {
9
+ console.error(`[connectxlab-mcp] env var manquante : ${key}`);
10
+ process.exit(1);
11
+ }
12
+ return v;
13
+ };
14
+ export const config = {
15
+ apiBaseUrl: process.env.XLAB_API_URL || 'https://v1.connectxlab.io/api/v1',
16
+ mcpToken: requireEnv('XLAB_MCP_TOKEN'),
17
+ timeoutMs: parseInt(process.env.XLAB_TIMEOUT_MS || '15000', 10),
18
+ };
package/dist/http.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Client axios preconfigure pour l'API Laravel. Toutes les requetes ajoutent
3
+ * automatiquement le Bearer + gerent les erreurs de facon standardisee pour
4
+ * que les tools MCP renvoient un message lisible par l'IA.
5
+ */
6
+ import axios, { AxiosError } from 'axios';
7
+ import { config } from './config.js';
8
+ export const http = axios.create({
9
+ baseURL: config.apiBaseUrl,
10
+ timeout: config.timeoutMs,
11
+ headers: {
12
+ Authorization: `Bearer ${config.mcpToken}`,
13
+ Accept: 'application/json',
14
+ },
15
+ });
16
+ /**
17
+ * Normalise une erreur axios en message compact que l'IA peut relire.
18
+ * Retourne toujours un string court prefixe par le status.
19
+ */
20
+ export function formatApiError(err) {
21
+ if (err instanceof AxiosError) {
22
+ const status = err.response?.status ?? 'network';
23
+ const body = err.response?.data;
24
+ const msg = (typeof body === 'object' && body && 'message' in body)
25
+ ? String(body.message)
26
+ : err.message;
27
+ return `[HTTP ${status}] ${msg}`;
28
+ }
29
+ return `[erreur] ${err.message ?? String(err)}`;
30
+ }
package/dist/index.js ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Serveur MCP ConnectXLab — expose les tools qui appellent l'API Laravel
4
+ * xlab-back. Se branche a Claude Desktop / Cursor / n'importe quel client
5
+ * MCP via stdio.
6
+ *
7
+ * Config attendue (env) :
8
+ * XLAB_MCP_TOKEN (requis) ton token cote ConnectXLab, prefixe xlab_mcp_
9
+ * XLAB_API_URL (defaut https://v1.connectxlab.io/api/v1)
10
+ * XLAB_TIMEOUT_MS (defaut 15000)
11
+ *
12
+ * Distribution :
13
+ * npx -y @connect-xlab/mcp-server (via config Claude Desktop, env transmis)
14
+ */
15
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
16
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
17
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
18
+ import { listLabsTool } from './tools/listLabs.js';
19
+ import { listMembersTool } from './tools/listMembers.js';
20
+ import { listPostsTool } from './tools/listPosts.js';
21
+ import { listCategoriesTool } from './tools/listCategories.js';
22
+ import { createPostTool } from './tools/createPost.js';
23
+ import { sendDmTool } from './tools/sendDm.js';
24
+ import { getEarningsTool } from './tools/getEarnings.js';
25
+ import { approveMemberTool } from './tools/approveMember.js';
26
+ import { rejectMemberTool } from './tools/rejectMember.js';
27
+ import { kickMemberTool } from './tools/kickMember.js';
28
+ import { listShopsTool } from './tools/listShops.js';
29
+ import { getShopSalesTool } from './tools/getShopSales.js';
30
+ import { listClassesTool } from './tools/listClasses.js';
31
+ import { createClassTool } from './tools/createClass.js';
32
+ import { deleteClassTool } from './tools/deleteClass.js';
33
+ import { listEventsTool } from './tools/listEvents.js';
34
+ import { createEventTool } from './tools/createEvent.js';
35
+ import { deleteEventTool } from './tools/deleteEvent.js';
36
+ import { pinPostTool } from './tools/pinPost.js';
37
+ import { listCommentsTool } from './tools/listComments.js';
38
+ import { addCommentTool } from './tools/addComment.js';
39
+ import { searchMembersTool } from './tools/searchMembers.js';
40
+ import { listAdminsTool } from './tools/listAdmins.js';
41
+ import { getLabSettingsTool } from './tools/getLabSettings.js';
42
+ import { updateLabSettingsTool } from './tools/updateLabSettings.js';
43
+ import { getStatsTool } from './tools/getStats.js';
44
+ import { updatePostTool } from './tools/updatePost.js';
45
+ import { deletePostTool } from './tools/deletePost.js';
46
+ import { updateClassTool } from './tools/updateClass.js';
47
+ import { banMemberTool } from './tools/banMember.js';
48
+ import { unbanMemberTool } from './tools/unbanMember.js';
49
+ import { promoteAdminTool } from './tools/promoteAdmin.js';
50
+ import { demoteAdminTool } from './tools/demoteAdmin.js';
51
+ import { createCategoryTool } from './tools/createCategory.js';
52
+ import { updateCategoryTool } from './tools/updateCategory.js';
53
+ import { deleteCategoryTool } from './tools/deleteCategory.js';
54
+ import { updateEventTool } from './tools/updateEvent.js';
55
+ import { listInvitationsTool } from './tools/listInvitations.js';
56
+ import { sendInvitationTool } from './tools/sendInvitation.js';
57
+ import { revokeInvitationTool } from './tools/revokeInvitation.js';
58
+ import { listPluginsTool } from './tools/listPlugins.js';
59
+ import { togglePluginTool } from './tools/togglePlugin.js';
60
+ import { setMetaPixelTool } from './tools/setMetaPixel.js';
61
+ import { setWelcomeDmTool } from './tools/setWelcomeDm.js';
62
+ import { listLevelsTool } from './tools/listLevels.js';
63
+ import { updateLevelTool } from './tools/updateLevel.js';
64
+ import { listLabLinksTool } from './tools/listLabLinks.js';
65
+ import { createLabLinkTool } from './tools/createLabLink.js';
66
+ import { updateLabLinkTool } from './tools/updateLabLink.js';
67
+ import { deleteLabLinkTool } from './tools/deleteLabLink.js';
68
+ import { newsletterDashboardTool } from './tools/newsletterDashboard.js';
69
+ import { listNewsletterEditionsTool } from './tools/listNewsletterEditions.js';
70
+ import { createNewsletterEditionTool } from './tools/createNewsletterEdition.js';
71
+ import { sendNewsletterEditionTool } from './tools/sendNewsletterEdition.js';
72
+ import { listNewsletterSubscribersTool } from './tools/listNewsletterSubscribers.js';
73
+ import { listPodcastShowsTool } from './tools/listPodcastShows.js';
74
+ import { createPodcastShowTool } from './tools/createPodcastShow.js';
75
+ import { listPodcastEpisodesTool } from './tools/listPodcastEpisodes.js';
76
+ import { listPodcastSubscribersTool } from './tools/listPodcastSubscribers.js';
77
+ import { updateSlugTool } from './tools/updateSlug.js';
78
+ import { listChannelSubscribersTool } from './tools/listChannelSubscribers.js';
79
+ import { createShopProductTool } from './tools/createShopProduct.js';
80
+ import { updateShopProductTool } from './tools/updateShopProduct.js';
81
+ import { deleteShopProductTool } from './tools/deleteShopProduct.js';
82
+ import { createPodcastEpisodeTool } from './tools/createPodcastEpisode.js';
83
+ import { updatePodcastEpisodeTool } from './tools/updatePodcastEpisode.js';
84
+ import { publishPodcastEpisodeTool } from './tools/publishPodcastEpisode.js';
85
+ import { unpublishPodcastEpisodeTool } from './tools/unpublishPodcastEpisode.js';
86
+ const tools = [
87
+ listLabsTool,
88
+ listMembersTool,
89
+ listPostsTool,
90
+ listCategoriesTool,
91
+ createPostTool,
92
+ sendDmTool,
93
+ getEarningsTool,
94
+ approveMemberTool,
95
+ rejectMemberTool,
96
+ kickMemberTool,
97
+ listShopsTool,
98
+ getShopSalesTool,
99
+ listClassesTool,
100
+ createClassTool,
101
+ deleteClassTool,
102
+ listEventsTool,
103
+ createEventTool,
104
+ deleteEventTool,
105
+ pinPostTool,
106
+ listCommentsTool,
107
+ addCommentTool,
108
+ searchMembersTool,
109
+ listAdminsTool,
110
+ getLabSettingsTool,
111
+ updateLabSettingsTool,
112
+ getStatsTool,
113
+ updatePostTool,
114
+ deletePostTool,
115
+ updateClassTool,
116
+ banMemberTool,
117
+ unbanMemberTool,
118
+ promoteAdminTool,
119
+ demoteAdminTool,
120
+ createCategoryTool,
121
+ updateCategoryTool,
122
+ deleteCategoryTool,
123
+ updateEventTool,
124
+ listInvitationsTool,
125
+ sendInvitationTool,
126
+ revokeInvitationTool,
127
+ listPluginsTool,
128
+ togglePluginTool,
129
+ setMetaPixelTool,
130
+ setWelcomeDmTool,
131
+ listLevelsTool,
132
+ updateLevelTool,
133
+ listLabLinksTool,
134
+ createLabLinkTool,
135
+ updateLabLinkTool,
136
+ deleteLabLinkTool,
137
+ newsletterDashboardTool,
138
+ listNewsletterEditionsTool,
139
+ createNewsletterEditionTool,
140
+ sendNewsletterEditionTool,
141
+ listNewsletterSubscribersTool,
142
+ listPodcastShowsTool,
143
+ createPodcastShowTool,
144
+ listPodcastEpisodesTool,
145
+ listPodcastSubscribersTool,
146
+ updateSlugTool,
147
+ listChannelSubscribersTool,
148
+ createShopProductTool,
149
+ updateShopProductTool,
150
+ deleteShopProductTool,
151
+ createPodcastEpisodeTool,
152
+ updatePodcastEpisodeTool,
153
+ publishPodcastEpisodeTool,
154
+ unpublishPodcastEpisodeTool,
155
+ ];
156
+ const server = new Server({ name: 'connectxlab', version: '0.1.0' }, { capabilities: { tools: {} } });
157
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
158
+ tools: tools.map(t => ({
159
+ name: t.name,
160
+ description: t.description,
161
+ inputSchema: t.inputSchema,
162
+ })),
163
+ }));
164
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
165
+ const { name, arguments: args } = request.params;
166
+ const tool = tools.find(t => t.name === name);
167
+ if (!tool) {
168
+ return {
169
+ isError: true,
170
+ content: [{ type: 'text', text: `Tool inconnu : ${name}` }],
171
+ };
172
+ }
173
+ return await tool.handler(args ?? {});
174
+ });
175
+ const transport = new StdioServerTransport();
176
+ await server.connect(transport);
177
+ // Log stderr pour debug (Claude Desktop l'affiche dans les logs).
178
+ console.error(`[connectxlab-mcp] ready — ${tools.length} tools : ${tools.map(t => t.name).join(', ')}`);
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import { http, formatApiError } from '../http.js';
3
+ export const addCommentTool = {
4
+ name: 'add_comment',
5
+ description: "Ajoute un commentaire a un post au nom de l'owner (le token).",
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ lab: { type: 'string' },
10
+ post_id: { type: 'number' },
11
+ body: { type: 'string', description: 'Contenu du commentaire (max 5000 car)' },
12
+ },
13
+ required: ['lab', 'post_id', 'body'],
14
+ additionalProperties: false,
15
+ },
16
+ zodSchema: z.object({
17
+ lab: z.string(),
18
+ post_id: z.number().int().positive(),
19
+ body: z.string().min(1).max(5000),
20
+ }),
21
+ async handler(args) {
22
+ const parsed = this.zodSchema.parse(args);
23
+ try {
24
+ const { data } = await http.post(`/mcp/labs/${encodeURIComponent(parsed.lab)}/posts/${parsed.post_id}/comments`, { body: parsed.body });
25
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
26
+ }
27
+ catch (err) {
28
+ return { isError: true, content: [{ type: 'text', text: formatApiError(err) }] };
29
+ }
30
+ },
31
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Tool `approve_member` — approuve un membre pending.
3
+ */
4
+ import { z } from 'zod';
5
+ import { http, formatApiError } from '../http.js';
6
+ export const approveMemberTool = {
7
+ name: 'approve_member',
8
+ description: "Approuve un membre en attente (pending -> approved). L'id est celui de list_members data[].id. Idempotent.",
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ lab: { type: 'string', description: 'Id ou url du Lab' },
13
+ member_id: { type: 'number', description: "Id du membre (list_members data[].id)" },
14
+ },
15
+ required: ['lab', 'member_id'],
16
+ additionalProperties: false,
17
+ },
18
+ zodSchema: z.object({ lab: z.string(), member_id: z.number().int().positive() }),
19
+ async handler(args) {
20
+ const parsed = this.zodSchema.parse(args);
21
+ try {
22
+ const { data } = await http.put(`/mcp/labs/${encodeURIComponent(parsed.lab)}/members/${parsed.member_id}/approve`);
23
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
24
+ }
25
+ catch (err) {
26
+ return { isError: true, content: [{ type: 'text', text: formatApiError(err) }] };
27
+ }
28
+ },
29
+ };
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ import { http, formatApiError } from '../http.js';
3
+ export const banMemberTool = {
4
+ name: 'ban_member',
5
+ description: "Bannit un membre du Lab : passe son status a 'blocked' (l'empeche de se reinscrire, different de kick qui autorise le retour). IMPORTANT : action forte, demander confirmation utilisateur.",
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ lab: { type: 'string' },
10
+ member_id: { type: 'number' },
11
+ },
12
+ required: ['lab', 'member_id'],
13
+ additionalProperties: false,
14
+ },
15
+ zodSchema: z.object({ lab: z.string(), member_id: z.number().int().positive() }),
16
+ async handler(args) {
17
+ const parsed = this.zodSchema.parse(args);
18
+ try {
19
+ const { data } = await http.put(`/mcp/labs/${encodeURIComponent(parsed.lab)}/members/${parsed.member_id}/ban`);
20
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
21
+ }
22
+ catch (err) {
23
+ return { isError: true, content: [{ type: 'text', text: formatApiError(err) }] };
24
+ }
25
+ },
26
+ };
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ import { http, formatApiError } from '../http.js';
3
+ export const createCategoryTool = {
4
+ name: 'create_category',
5
+ description: "Cree une categorie pour classer les posts du Lab. is_private cache la categorie aux non-abonnes payants.",
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ lab: { type: 'string' },
10
+ name: { type: 'string', maxLength: 30 },
11
+ description: { type: 'string', maxLength: 255 },
12
+ is_private: { type: 'boolean' },
13
+ min_user_level: { type: 'number', minimum: 0 },
14
+ },
15
+ required: ['lab', 'name', 'description'],
16
+ additionalProperties: false,
17
+ },
18
+ zodSchema: z.object({
19
+ lab: z.string(),
20
+ name: z.string().max(30),
21
+ description: z.string().max(255),
22
+ is_private: z.boolean().optional(),
23
+ min_user_level: z.number().int().min(0).nullable().optional(),
24
+ }),
25
+ async handler(args) {
26
+ const parsed = this.zodSchema.parse(args);
27
+ const { lab, ...body } = parsed;
28
+ try {
29
+ const { data } = await http.post(`/mcp/labs/${encodeURIComponent(lab)}/categories`, body);
30
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
31
+ }
32
+ catch (err) {
33
+ return { isError: true, content: [{ type: 'text', text: formatApiError(err) }] };
34
+ }
35
+ },
36
+ };
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ import { http, formatApiError } from '../http.js';
3
+ export const createClassTool = {
4
+ name: 'create_class',
5
+ description: "Cree une classe/cours dans un Lab. name max 50 car, description max 400 car.",
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ lab: { type: 'string' },
10
+ name: { type: 'string', description: 'Titre de la classe (max 50 car)' },
11
+ description: { type: 'string', description: 'Description (max 400 car)' },
12
+ access: { type: 'string', description: "Type d'acces (ex: 'all', 'ONLY'). Defaut 'all'." },
13
+ min_user_level: { type: 'number', description: 'Niveau membre requis (optionnel)' },
14
+ is_published: { type: 'boolean', default: false },
15
+ },
16
+ required: ['lab', 'name', 'description'],
17
+ additionalProperties: false,
18
+ },
19
+ zodSchema: z.object({
20
+ lab: z.string(),
21
+ name: z.string().max(50),
22
+ description: z.string().max(400),
23
+ access: z.string().optional(),
24
+ min_user_level: z.number().int().min(0).optional(),
25
+ is_published: z.boolean().optional(),
26
+ }),
27
+ async handler(args) {
28
+ const parsed = this.zodSchema.parse(args);
29
+ try {
30
+ const { data } = await http.post(`/mcp/labs/${encodeURIComponent(parsed.lab)}/classes`, parsed);
31
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
32
+ }
33
+ catch (err) {
34
+ return { isError: true, content: [{ type: 'text', text: formatApiError(err) }] };
35
+ }
36
+ },
37
+ };
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod';
2
+ import { http, formatApiError } from '../http.js';
3
+ export const createEventTool = {
4
+ name: 'create_event',
5
+ description: "Cree un evenement dans un Lab. event_date au format ISO 8601 (ex: 2026-08-15T18:00:00Z). timezone au format IANA (ex: Europe/Paris).",
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ lab: { type: 'string' },
10
+ title: { type: 'string', description: 'Titre (max 50 car)' },
11
+ description: { type: 'string' },
12
+ event_date: { type: 'string', description: 'Date/heure ISO 8601' },
13
+ timezone: { type: 'string', description: 'IANA (ex: Europe/Paris)' },
14
+ duration: { type: 'string', description: 'ex: "1 hour", "90 min"' },
15
+ reminder_enabled: { type: 'boolean', default: false },
16
+ },
17
+ required: ['lab', 'title', 'description', 'event_date', 'timezone', 'duration'],
18
+ additionalProperties: false,
19
+ },
20
+ zodSchema: z.object({
21
+ lab: z.string(),
22
+ title: z.string().max(50),
23
+ description: z.string(),
24
+ event_date: z.string(),
25
+ timezone: z.string(),
26
+ duration: z.string(),
27
+ reminder_enabled: z.boolean().optional(),
28
+ }),
29
+ async handler(args) {
30
+ const parsed = this.zodSchema.parse(args);
31
+ try {
32
+ const { data } = await http.post(`/mcp/labs/${encodeURIComponent(parsed.lab)}/events`, parsed);
33
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
34
+ }
35
+ catch (err) {
36
+ return { isError: true, content: [{ type: 'text', text: formatApiError(err) }] };
37
+ }
38
+ },
39
+ };
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import { http, formatApiError } from '../http.js';
3
+ export const createLabLinkTool = {
4
+ name: 'create_lab_link',
5
+ description: "Cree un lien sidebar custom (label + url). Max 5 par Lab. Position ajoutee automatiquement a la fin.",
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ lab: { type: 'string' },
10
+ label: { type: 'string', maxLength: 30 },
11
+ url: { type: 'string', maxLength: 255 },
12
+ },
13
+ required: ['lab', 'label', 'url'],
14
+ additionalProperties: false,
15
+ },
16
+ zodSchema: z.object({ lab: z.string(), label: z.string().max(30), url: z.string().max(255) }),
17
+ async handler(args) {
18
+ const parsed = this.zodSchema.parse(args);
19
+ const { lab, ...body } = parsed;
20
+ try {
21
+ const { data } = await http.post(`/mcp/labs/${encodeURIComponent(lab)}/links`, body);
22
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
23
+ }
24
+ catch (err) {
25
+ return { isError: true, content: [{ type: 'text', text: formatApiError(err) }] };
26
+ }
27
+ },
28
+ };
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ import { http, formatApiError } from '../http.js';
3
+ export const createNewsletterEditionTool = {
4
+ name: 'create_newsletter_edition',
5
+ description: "Cree un draft d'edition newsletter (title + subject + body HTML). L'envoi se fait via send_newsletter_edition.",
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ lab: { type: 'string' },
10
+ title: { type: 'string', maxLength: 255 },
11
+ subject: { type: 'string', maxLength: 255, description: "Objet email" },
12
+ body_html: { type: 'string', description: "Corps HTML" },
13
+ access: { type: 'string', enum: ['free', 'premium'] },
14
+ audience: { type: 'string', enum: ['all', 'premium', 'free', 'lab_members'] },
15
+ },
16
+ required: ['lab', 'title', 'subject', 'body_html'],
17
+ additionalProperties: false,
18
+ },
19
+ zodSchema: z.object({
20
+ lab: z.string(),
21
+ title: z.string().max(255),
22
+ subject: z.string().max(255),
23
+ body_html: z.string(),
24
+ access: z.enum(['free', 'premium']).optional(),
25
+ audience: z.enum(['all', 'premium', 'free', 'lab_members']).optional(),
26
+ }),
27
+ async handler(args) {
28
+ const p = this.zodSchema.parse(args);
29
+ const { lab, ...body } = p;
30
+ try {
31
+ const { data } = await http.post(`/mcp/labs/${encodeURIComponent(lab)}/newsletter/editions`, body);
32
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
33
+ }
34
+ catch (err) {
35
+ return { isError: true, content: [{ type: 'text', text: formatApiError(err) }] };
36
+ }
37
+ },
38
+ };
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ import { http, formatApiError } from '../http.js';
3
+ export const createPodcastEpisodeTool = {
4
+ name: 'create_podcast_episode',
5
+ description: "Cree un episode (draft) dans un show. Audio a uploader ensuite via le front. Numero d'episode assigne automatiquement.",
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ lab: { type: 'string' },
10
+ show_id: { type: 'number' },
11
+ title: { type: 'string', maxLength: 255 },
12
+ description: { type: 'string' },
13
+ access: { type: 'string', enum: ['free', 'premium'] },
14
+ season: { type: 'number', minimum: 1 },
15
+ },
16
+ required: ['lab', 'show_id', 'title'],
17
+ additionalProperties: false,
18
+ },
19
+ zodSchema: z.object({
20
+ lab: z.string(),
21
+ show_id: z.number().int().positive(),
22
+ title: z.string().max(255),
23
+ description: z.string().nullable().optional(),
24
+ access: z.enum(['free', 'premium']).optional(),
25
+ season: z.number().int().min(1).optional(),
26
+ }),
27
+ async handler(args) {
28
+ const p = this.zodSchema.parse(args);
29
+ const { lab, show_id, ...body } = p;
30
+ try {
31
+ const { data } = await http.post(`/mcp/labs/${encodeURIComponent(lab)}/podcast/shows/${show_id}/episodes`, body);
32
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
33
+ }
34
+ catch (err) {
35
+ return { isError: true, content: [{ type: 'text', text: formatApiError(err) }] };
36
+ }
37
+ },
38
+ };
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ import { http, formatApiError } from '../http.js';
3
+ export const createPodcastShowTool = {
4
+ name: 'create_podcast_show',
5
+ description: "Cree un nouveau show podcast dans le Lab (titre + description + auteur). Le slug est genere automatiquement.",
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ lab: { type: 'string' },
10
+ title: { type: 'string', maxLength: 255 },
11
+ description: { type: 'string' },
12
+ author_name: { type: 'string', maxLength: 191 },
13
+ },
14
+ required: ['lab', 'title'],
15
+ additionalProperties: false,
16
+ },
17
+ zodSchema: z.object({
18
+ lab: z.string(),
19
+ title: z.string().max(255),
20
+ description: z.string().nullable().optional(),
21
+ author_name: z.string().max(191).nullable().optional(),
22
+ }),
23
+ async handler(args) {
24
+ const p = this.zodSchema.parse(args);
25
+ const { lab, ...body } = p;
26
+ try {
27
+ const { data } = await http.post(`/mcp/labs/${encodeURIComponent(lab)}/podcast/shows`, body);
28
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
29
+ }
30
+ catch (err) {
31
+ return { isError: true, content: [{ type: 'text', text: formatApiError(err) }] };
32
+ }
33
+ },
34
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Tool `create_post` — cree un post dans un Lab. Necessite un category_id
3
+ * valide (recupere via list_categories).
4
+ */
5
+ import { z } from 'zod';
6
+ import { http, formatApiError } from '../http.js';
7
+ export const createPostTool = {
8
+ name: 'create_post',
9
+ description: "Cree un nouveau post dans un Lab. Le body peut etre du texte simple (sera wrappe en <p>) ou du HTML. Necessite un category_id valide — utilise list_categories pour l'obtenir.",
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: {
13
+ lab: { type: 'string', description: 'Id ou url du Lab' },
14
+ title: { type: 'string', description: 'Titre du post (max 255 char)' },
15
+ body: { type: 'string', description: 'Contenu du post — texte ou HTML' },
16
+ category_id: { type: 'number', description: "Id d'une categorie du Lab (list_categories)" },
17
+ },
18
+ required: ['lab', 'title', 'body', 'category_id'],
19
+ additionalProperties: false,
20
+ },
21
+ zodSchema: z.object({
22
+ lab: z.string(),
23
+ title: z.string().min(1).max(255),
24
+ body: z.string().min(1),
25
+ category_id: z.number().int().positive(),
26
+ }),
27
+ async handler(args) {
28
+ const parsed = this.zodSchema.parse(args);
29
+ try {
30
+ const { data } = await http.post(`/mcp/labs/${encodeURIComponent(parsed.lab)}/posts`, {
31
+ title: parsed.title,
32
+ body: parsed.body,
33
+ category_id: parsed.category_id,
34
+ });
35
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
36
+ }
37
+ catch (err) {
38
+ return { isError: true, content: [{ type: 'text', text: formatApiError(err) }] };
39
+ }
40
+ },
41
+ };
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+ import { http, formatApiError } from '../http.js';
3
+ export const createShopProductTool = {
4
+ name: 'create_shop_product',
5
+ description: "Cree un produit dans la boutique du Lab. Files (bannieres, PDF) non geres par MCP — a uploader ensuite via le front. Prix en centimes (price_cents).",
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ lab: { type: 'string' },
10
+ title: { type: 'string', maxLength: 255 },
11
+ short_description: { type: 'string' },
12
+ description: { type: 'string' },
13
+ price_cents: { type: 'number', minimum: 0, description: 'Prix en centimes (ex 2990 = 29.90 EUR)' },
14
+ offer_type: { type: 'string', enum: ['digital', 'physical', 'calendar', 'service'] },
15
+ payment_mode: { type: 'string', enum: ['external', 'stripe'] },
16
+ payment_link_text: { type: 'string', maxLength: 255 },
17
+ support_email: { type: 'string', format: 'email' },
18
+ is_published: { type: 'boolean' },
19
+ },
20
+ required: ['lab', 'title', 'short_description', 'description'],
21
+ additionalProperties: false,
22
+ },
23
+ zodSchema: z.object({
24
+ lab: z.string(),
25
+ title: z.string().max(255),
26
+ short_description: z.string(),
27
+ description: z.string(),
28
+ price_cents: z.number().int().min(0).nullable().optional(),
29
+ offer_type: z.enum(['digital', 'physical', 'calendar', 'service']).optional(),
30
+ payment_mode: z.enum(['external', 'stripe']).optional(),
31
+ payment_link_text: z.string().max(255).nullable().optional(),
32
+ support_email: z.string().email().nullable().optional(),
33
+ is_published: z.boolean().optional(),
34
+ }),
35
+ async handler(args) {
36
+ const p = this.zodSchema.parse(args);
37
+ const { lab, ...body } = p;
38
+ try {
39
+ const { data } = await http.post(`/mcp/labs/${encodeURIComponent(lab)}/shops`, body);
40
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
41
+ }
42
+ catch (err) {
43
+ return { isError: true, content: [{ type: 'text', text: formatApiError(err) }] };
44
+ }
45
+ },
46
+ };