create-email-renderer 0.1.1 → 0.3.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 (49) hide show
  1. package/README.md +106 -2
  2. package/SKILL.md +217 -0
  3. package/dist/build.d.ts +52 -0
  4. package/dist/build.d.ts.map +1 -0
  5. package/dist/build.js +169 -0
  6. package/dist/build.js.map +1 -0
  7. package/dist/default-blocks.d.ts.map +1 -1
  8. package/dist/default-blocks.js +377 -5
  9. package/dist/default-blocks.js.map +1 -1
  10. package/dist/html-render.d.ts +4 -2
  11. package/dist/html-render.d.ts.map +1 -1
  12. package/dist/html-render.js +758 -57
  13. package/dist/html-render.js.map +1 -1
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/normalize.d.ts +10 -2
  19. package/dist/normalize.d.ts.map +1 -1
  20. package/dist/normalize.js +166 -38
  21. package/dist/normalize.js.map +1 -1
  22. package/dist/records.d.ts +13 -0
  23. package/dist/records.d.ts.map +1 -0
  24. package/dist/records.js +93 -0
  25. package/dist/records.js.map +1 -0
  26. package/dist/registry.d.ts +35 -0
  27. package/dist/registry.d.ts.map +1 -0
  28. package/dist/registry.js +31 -0
  29. package/dist/registry.js.map +1 -0
  30. package/dist/richtext.d.ts +6 -0
  31. package/dist/richtext.d.ts.map +1 -1
  32. package/dist/richtext.js +13 -0
  33. package/dist/richtext.js.map +1 -1
  34. package/dist/server.d.ts +4 -2
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +2 -1
  37. package/dist/server.js.map +1 -1
  38. package/dist/tracking.d.ts +12 -0
  39. package/dist/tracking.d.ts.map +1 -0
  40. package/dist/tracking.js +78 -0
  41. package/dist/tracking.js.map +1 -0
  42. package/dist/types.d.ts +407 -7
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js +159 -0
  45. package/dist/types.js.map +1 -1
  46. package/docs/BLOCKS.md +530 -0
  47. package/docs/MCP.md +189 -0
  48. package/llms.txt +17 -0
  49. package/package.json +17 -4
package/docs/MCP.md ADDED
@@ -0,0 +1,189 @@
1
+ # Exponer `create-email-renderer` como MCP
2
+
3
+ Guía para montar un **servidor MCP** (Model Context Protocol) en el sistema donde ya está
4
+ instalado el paquete, de forma que un agente pueda listar bloques, construir correos y
5
+ renderizarlos sin conocer el JSON a mano.
6
+
7
+ El renderer es JS puro **sin React ni DOM**, así que el MCP puede correr en **Node** (stdio) o en
8
+ un **Worker** (HTTP/SSE) con el mismo código de dominio.
9
+
10
+ > Antes de escribir tools, ten a mano [`docs/BLOCKS.md`](./BLOCKS.md): es la referencia que
11
+ > deben leer los prompts para elegir `type` y props.
12
+
13
+ ---
14
+
15
+ ## Tools recomendadas
16
+
17
+ Mapeo directo a la API real, sin lógica de negocio propia:
18
+
19
+ | Tool | Input | Output | API que envuelve |
20
+ |---|---|---|---|
21
+ | `list_blocks` | `{}` | `[{ type, label, description, defaultProps }]` | `listBlockDefinitions()` (registry vivo, incluye plugins) |
22
+ | `get_block_schema` | `{ type }` | `{ type, label, description, defaultProps }` o `null` | `listBlockDefinitions().find(...)` |
23
+ | `build_block` | `{ type, props? }` | `EmailBlock` o `null` | `blockJson(type, props)` |
24
+ | `render_email` | `{ subject, blocks?, payload?, context?, settings?, tracking? }` | `{ html, subject }` | `templateJson` + `renderTemplateEmail` |
25
+ | `parse_template` | `{ payload }` | `{ content, settings }` | `parseTemplatePayload(payload)` |
26
+ | `validate_template` | `{ payload }` | `{ ok, warnings[], blocks }` | `parseTemplatePayload` + chequeos propios |
27
+
28
+ ### Detalles de diseño
29
+
30
+ - **`list_blocks` primero**: es lo que permite que el modelo no invente tipos. Devuelve el
31
+ `defaultProps` de cada bloque, que ya es el "esquema real" (con `type`/`label`/`description`).
32
+ - **`build_block` nunca debería recibir un `id`**: `blockJson` lo genera. Si el modelo se empeña,
33
+ ignóralo.
34
+ - **`render_email` acepta `blocks` (array) o `payload` (lo guardado)**; si vienen ambos, gana
35
+ `payload`.
36
+ - **Archivos**: `settings.files` viaja dentro del payload; `render_email` lista en el HTML los que
37
+ tengan `link !== false` y tú decides los adjuntos reales con los que tengan `attach: true`
38
+ (`parse_template` te devuelve los settings ya normalizados).
39
+ - **`validate_template`** es el guardarraíl: cuenta los bloques descartados por tipo desconocido,
40
+ avisa de `href` vacíos en CTAs, de imágenes sin `src` y del peso del HTML (Gmail recorta a
41
+ ~102 KB).
42
+ - **Estados**: no inventes tools que muten la plantilla guardada; el renderer solo transforma
43
+ JSON a HTML. El guardado es de tu backend.
44
+
45
+ ## Ejemplo mínimo (Node, stdio)
46
+
47
+ ```ts
48
+ // mcp-server.ts — `npm i create-email-renderer @modelcontextprotocol/sdk zod`
49
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
50
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
51
+ import { z } from "zod";
52
+ import {
53
+ blockJson,
54
+ listBlockDefinitions,
55
+ renderTemplateEmail,
56
+ parseTemplatePayload,
57
+ templateJson,
58
+ } from "create-email-renderer";
59
+
60
+ const server = new McpServer({ name: "email-builder", version: "1.0.0" });
61
+
62
+ server.tool("list_blocks", {}, async () => ({
63
+ content: [
64
+ {
65
+ type: "text",
66
+ text: JSON.stringify(
67
+ listBlockDefinitions().map((d) => ({
68
+ type: d.type,
69
+ label: d.label,
70
+ description: d.description,
71
+ defaultProps: d.defaultProps,
72
+ })),
73
+ ),
74
+ },
75
+ ],
76
+ }));
77
+
78
+ server.tool(
79
+ "build_block",
80
+ { type: z.string(), props: z.record(z.unknown()).optional() },
81
+ async ({ type, props }) => {
82
+ const block = blockJson(type, props as never);
83
+ return {
84
+ content: [
85
+ {
86
+ type: "text",
87
+ text: block
88
+ ? JSON.stringify(block)
89
+ : `Tipo desconocido "${type}". Usa list_blocks para ver los válidos.`,
90
+ },
91
+ ],
92
+ };
93
+ },
94
+ );
95
+
96
+ server.tool(
97
+ "render_email",
98
+ {
99
+ subject: z.string(),
100
+ blocks: z.array(z.unknown()).optional(),
101
+ payload: z.unknown().optional(),
102
+ context: z.record(z.string()).optional(),
103
+ settings: z.record(z.unknown()).optional(),
104
+ tracking: z.record(z.unknown()).optional(),
105
+ },
106
+ async ({ subject, blocks, payload, context, settings, tracking }) => {
107
+ const { html, subject: resolved } = await renderTemplateEmail({
108
+ subject,
109
+ payload: payload ?? templateJson((blocks ?? []) as never, settings as never),
110
+ context,
111
+ settings: settings as never,
112
+ tracking: tracking as never,
113
+ });
114
+ return { content: [{ type: "text", text: html }], structuredContent: { subject: resolved, html } };
115
+ },
116
+ );
117
+
118
+ server.tool(
119
+ "parse_template",
120
+ { payload: z.unknown() },
121
+ async ({ payload }) => ({
122
+ content: [{ type: "text", text: JSON.stringify(parseTemplatePayload(payload)) }],
123
+ }),
124
+ );
125
+
126
+ server.tool(
127
+ "validate_template",
128
+ { payload: z.unknown() },
129
+ async ({ payload }) => {
130
+ const { content, settings } = parseTemplatePayload(payload);
131
+ const warnings: string[] = [];
132
+ if (content.length === 0) warnings.push("No queda ningún bloque tras normalizar.");
133
+ const count = (t: string) => content.filter((b) => b.type === t).length;
134
+ for (const t of ["checkout", "product", "pricing"]) {
135
+ if (count(t) > 0 && !JSON.stringify(content.find((b) => b.type === t)).includes("https://")) {
136
+ warnings.push(`${t}: revisa que tenga enlaces/imágenes reales.`);
137
+ }
138
+ }
139
+ return {
140
+ content: [{ type: "text", text: JSON.stringify({ ok: warnings.length === 0, warnings, blocks: content.length }, null, 2) }],
141
+ structuredContent: { ok: warnings.length === 0, warnings, content, settings },
142
+ };
143
+ },
144
+ );
145
+
146
+ await server.connect(new StdioServerTransport());
147
+ ```
148
+
149
+ Registro en el cliente MCP (ejemplo Claude Desktop / opencode):
150
+
151
+ ```json
152
+ {
153
+ "mcpServers": {
154
+ "email-builder": {
155
+ "command": "node",
156
+ "args": ["/ruta/absoluta/mcp-server.js"]
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ ## Variante Cloudflare Workers (HTTP)
163
+
164
+ - Igual que arriba, pero en vez de `StdioServerTransport` monta el handler en un `fetch` y aplica
165
+ **`nodejs_compat`** si tu SDK lo pide.
166
+ - El renderer ya está pensado para Workers: `id.ts` no usa `crypto` ni `Math.random`, y no hay
167
+ dependencias de Node.
168
+
169
+ ## Prompt sugerido para el agente
170
+
171
+ ```
172
+ Eres un asistente que arma correos con create-email-renderer.
173
+ Flujo obligatorio:
174
+ 1) list_blocks para conocer los tipos y sus defaultProps.
175
+ 2) build_block(type, props) para cada bloque (NO escribas el JSON a mano ni pongas ids).
176
+ 3) render_email({ subject, blocks, context }) para obtener el HTML.
177
+ 4) validate_template({ payload }) antes de dar por bueno el correo.
178
+ Reglas: variables con {clave}; imágenes con URLs https (nunca data:);
179
+ los registros van en lines/products/images/links/entries/plans (nunca items);
180
+ un solo nivel de anidamiento; un footer por correo.
181
+ ```
182
+
183
+ ## Checklist antes de publicar el MCP
184
+
185
+ - [ ] `list_blocks` devuelve los 25 tipos (o los del registry que registres).
186
+ - [ ] `build_block` con un tipo inválido responde un mensaje claro, no un crash.
187
+ - [ ] `render_email` con `payload` inválido no tumba el server (el renderer ya tolera basura).
188
+ - [ ] `validate_template` avisa de bloques descartados, `href` vacíos, imágenes sin `src` y peso > 102 KB.
189
+ - [ ] Nada de estado global mutable: cada tool es una función pura sobre el JSON.
package/llms.txt ADDED
@@ -0,0 +1,17 @@
1
+ # create-email-renderer
2
+
3
+ > React-free email template renderer: JSON blocks -> email-safe HTML for back-ends, Workers and edge.
4
+
5
+ ## Docs
6
+ - [SKILL.md](./SKILL.md): orientation for agents — mental model, flows, recipes, golden rules.
7
+ - [docs/BLOCKS.md](./docs/BLOCKS.md): catalog of the 26 built-in blocks (props, defaults, variants, examples).
8
+ - [docs/MCP.md](./docs/MCP.md): how to expose this package as an MCP server (tools + code).
9
+ - [README.md](./README.md): public API reference (renderTemplateEmail, blockJson, tracking, typography).
10
+
11
+ ## Quick start
12
+ - `renderTemplateEmail({ subject, payload, context, settings, tracking })` -> `{ html, subject }`
13
+ - `blockJson(type, props?)` / `templateJson(blocks, settings?)` -> build a payload in code
14
+ - `parseTemplatePayload(raw)` -> `{ content, settings }` (never throws)
15
+ - `settings.files: EmailFileAttachment[]` -> downloads (`downloads` block) and real attachments (`attach: true`, resolved by your ESP)
16
+ - `normalizeFiles(input)` -> repairs a file list (ids, names, sizes, scheme whitelist)
17
+ - Subpaths: /server /html-render /build /normalize /types /variables /richtext /default-blocks /registry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-email-renderer",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "React-free email template renderer (JSON blocks -> email-safe HTML) for back-ends and edge runtimes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -37,10 +37,21 @@
37
37
  "./server": {
38
38
  "types": "./dist/server.d.ts",
39
39
  "import": "./dist/server.js"
40
+ },
41
+ "./registry": {
42
+ "types": "./dist/registry.d.ts",
43
+ "import": "./dist/registry.js"
44
+ },
45
+ "./build": {
46
+ "types": "./dist/build.d.ts",
47
+ "import": "./dist/build.js"
40
48
  }
41
49
  },
42
50
  "files": [
43
- "dist"
51
+ "dist",
52
+ "SKILL.md",
53
+ "llms.txt",
54
+ "docs"
44
55
  ],
45
56
  "dependencies": {
46
57
  "sanitize-html": "^2.17.6"
@@ -48,7 +59,8 @@
48
59
  "devDependencies": {
49
60
  "@types/node": "^24",
50
61
  "@types/sanitize-html": "^2.13.0",
51
- "typescript": "~6"
62
+ "typescript": "~6",
63
+ "vitest": "^5.0.1"
52
64
  },
53
65
  "license": "MIT",
54
66
  "author": "asmel2020",
@@ -65,6 +77,7 @@
65
77
  "build": "tsc -p tsconfig.json",
66
78
  "lint": "tsc --noEmit",
67
79
  "typecheck": "tsc --noEmit",
68
- "check-types": "tsc --noEmit"
80
+ "check-types": "tsc --noEmit",
81
+ "test": "vitest run"
69
82
  }
70
83
  }