tailprint 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +160 -2
- package/dist/config/env.d.ts +77 -0
- package/dist/config/env.js +38 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/plans.d.ts +15 -0
- package/dist/config/plans.js +104 -0
- package/dist/config/plans.js.map +1 -0
- package/dist/config/templates.d.ts +10 -0
- package/dist/config/templates.js +746 -0
- package/dist/config/templates.js.map +1 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +897 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +2 -0
- package/dist/mcp/stdio.js +27 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/routes/dashboard.d.ts +2 -0
- package/dist/routes/dashboard.js +454 -0
- package/dist/routes/dashboard.js.map +1 -0
- package/dist/routes/mcp.d.ts +2 -0
- package/dist/routes/mcp.js +329 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/render.d.ts +2 -0
- package/dist/routes/render.js +444 -0
- package/dist/routes/render.js.map +1 -0
- package/dist/routes/scrape.d.ts +2 -0
- package/dist/routes/scrape.js +132 -0
- package/dist/routes/scrape.js.map +1 -0
- package/dist/routes/templates.d.ts +2 -0
- package/dist/routes/templates.js +2449 -0
- package/dist/routes/templates.js.map +1 -0
- package/dist/routes/usage.d.ts +31 -0
- package/dist/routes/usage.js +73 -0
- package/dist/routes/usage.js.map +1 -0
- package/dist/routes/webhooks.d.ts +2 -0
- package/dist/routes/webhooks.js +227 -0
- package/dist/routes/webhooks.js.map +1 -0
- package/dist/schemas/render.d.ts +422 -0
- package/dist/schemas/render.js +91 -0
- package/dist/schemas/render.js.map +1 -0
- package/dist/schemas/scrape.d.ts +65 -0
- package/dist/schemas/scrape.js +24 -0
- package/dist/schemas/scrape.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +206 -0
- package/dist/server.js.map +1 -0
- package/dist/services/browser.d.ts +21 -0
- package/dist/services/browser.js +299 -0
- package/dist/services/browser.js.map +1 -0
- package/dist/services/carousel.d.ts +43 -0
- package/dist/services/carousel.js +300 -0
- package/dist/services/carousel.js.map +1 -0
- package/dist/services/chart.d.ts +29 -0
- package/dist/services/chart.js +129 -0
- package/dist/services/chart.js.map +1 -0
- package/dist/services/eInvoice.d.ts +62 -0
- package/dist/services/eInvoice.js +413 -0
- package/dist/services/eInvoice.js.map +1 -0
- package/dist/services/email.d.ts +24 -0
- package/dist/services/email.js +177 -0
- package/dist/services/email.js.map +1 -0
- package/dist/services/mockup.d.ts +14 -0
- package/dist/services/mockup.js +130 -0
- package/dist/services/mockup.js.map +1 -0
- package/dist/services/pdfStamper.d.ts +16 -0
- package/dist/services/pdfStamper.js +47 -0
- package/dist/services/pdfStamper.js.map +1 -0
- package/dist/services/qr.d.ts +20 -0
- package/dist/services/qr.js +44 -0
- package/dist/services/qr.js.map +1 -0
- package/dist/services/redis.d.ts +65 -0
- package/dist/services/redis.js +318 -0
- package/dist/services/redis.js.map +1 -0
- package/dist/services/scraper.d.ts +16 -0
- package/dist/services/scraper.js +138 -0
- package/dist/services/scraper.js.map +1 -0
- package/dist/services/storage.d.ts +38 -0
- package/dist/services/storage.js +123 -0
- package/dist/services/storage.js.map +1 -0
- package/dist/services/stripe.d.ts +32 -0
- package/dist/services/stripe.js +229 -0
- package/dist/services/stripe.js.map +1 -0
- package/dist/services/visualDiff.d.ts +33 -0
- package/dist/services/visualDiff.js +218 -0
- package/dist/services/visualDiff.js.map +1 -0
- package/package.json +74 -4
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { browserService } from '../services/browser.js';
|
|
4
|
+
import { scraperService } from '../services/scraper.js';
|
|
5
|
+
import { qrService } from '../services/qr.js';
|
|
6
|
+
import { chartService } from '../services/chart.js';
|
|
7
|
+
import { mockupService } from '../services/mockup.js';
|
|
8
|
+
import { pdfStamperService } from '../services/pdfStamper.js';
|
|
9
|
+
import { storageService } from '../services/storage.js';
|
|
10
|
+
import { visualDiffService } from '../services/visualDiff.js';
|
|
11
|
+
import { carouselService } from '../services/carousel.js';
|
|
12
|
+
import { eInvoiceService } from '../services/eInvoice.js';
|
|
13
|
+
import { TEMPLATES } from '../config/templates.js';
|
|
14
|
+
const TEMPLATE_IDS = [
|
|
15
|
+
'invoice',
|
|
16
|
+
'certificate',
|
|
17
|
+
'report',
|
|
18
|
+
'resume',
|
|
19
|
+
'og-card',
|
|
20
|
+
'shipping-label',
|
|
21
|
+
'ticket',
|
|
22
|
+
'receipt',
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Creates and configures an McpServer instance with Tailprint Tools, Resources, and Prompts.
|
|
26
|
+
*/
|
|
27
|
+
export function createMcpServer(options = {}) {
|
|
28
|
+
const server = new McpServer({
|
|
29
|
+
name: options.name || 'tailprint-mcp',
|
|
30
|
+
version: options.version || '1.0.0',
|
|
31
|
+
});
|
|
32
|
+
// ==========================================
|
|
33
|
+
// MCP TOOLS
|
|
34
|
+
// ==========================================
|
|
35
|
+
// Tool 1: scrape_markdown
|
|
36
|
+
server.tool('scrape_markdown', 'Scrapes any public URL using headless Chromium, strips ads/scripts/trackers with Readability, and converts the content into clean, LLM-optimized Markdown with token savings metrics.', {
|
|
37
|
+
url: z.string().url().describe('The full web URL to scrape (e.g. https://techcrunch.com/article)'),
|
|
38
|
+
strip_images: z.boolean().optional().default(true).describe('Whether to remove image tags to minimize token count for LLMs (default: true)'),
|
|
39
|
+
wait_for: z.string().optional().describe('Optional CSS selector to wait for before parsing (e.g. "article", "main", ".content")'),
|
|
40
|
+
}, async ({ url, strip_images, wait_for }) => {
|
|
41
|
+
try {
|
|
42
|
+
if (options.onUsage) {
|
|
43
|
+
await options.onUsage('scrape_markdown');
|
|
44
|
+
}
|
|
45
|
+
const result = await scraperService.scrapeUrl({
|
|
46
|
+
url,
|
|
47
|
+
strip_images: strip_images ?? true,
|
|
48
|
+
include_raw_html: false,
|
|
49
|
+
timeout_ms: 8000,
|
|
50
|
+
wait_for,
|
|
51
|
+
});
|
|
52
|
+
const headerInfo = [
|
|
53
|
+
`# ${result.title}`,
|
|
54
|
+
result.byline ? `**Author:** ${result.byline}` : null,
|
|
55
|
+
result.published_at ? `**Published:** ${result.published_at}` : null,
|
|
56
|
+
`**URL:** ${result.url}`,
|
|
57
|
+
`**Token Metrics:** ~${result.estimated_tokens} tokens (${result.word_count} words) | Saved: ${result.token_savings_percent}`,
|
|
58
|
+
'---',
|
|
59
|
+
].filter(Boolean).join('\n');
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: `${headerInfo}\n\n${result.markdown}`,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return {
|
|
71
|
+
isError: true,
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: `Error scraping URL "${url}": ${err.message || 'Unknown error'}`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// Tool 2: render_pdf
|
|
82
|
+
server.tool('render_pdf', 'Converts HTML with full Tailwind CSS classes into a pixel-perfect, vector PDF document. Returns the document as base64 data URI.', {
|
|
83
|
+
html: z.string().optional().describe('HTML markup to render. Tailwind CSS utility classes are supported automatically. Either html or url is required.'),
|
|
84
|
+
url: z.string().url().optional().describe('Public web URL to render directly to PDF (e.g. https://news.ycombinator.com). Either html or url is required.'),
|
|
85
|
+
format: z.enum(['A4', 'Letter']).optional().default('A4').describe('Paper format: "A4" or "Letter" (default: "A4")'),
|
|
86
|
+
landscape: z.boolean().optional().default(false).describe('Whether to generate PDF in landscape orientation (default: false)'),
|
|
87
|
+
print_background: z.boolean().optional().default(true).describe('Whether to print CSS backgrounds and colors (default: true)'),
|
|
88
|
+
header_html: z.string().optional().describe('Optional HTML template for page header (e.g. with pageNumber / totalPages)'),
|
|
89
|
+
footer_html: z.string().optional().describe('Optional HTML template for page footer (e.g. with pageNumber / totalPages)'),
|
|
90
|
+
password: z.string().optional().describe('Optional password to encrypt the PDF with AES-256'),
|
|
91
|
+
wait_for: z.string().optional().describe('Optional CSS selector to wait for before rendering'),
|
|
92
|
+
delay_ms: z.number().optional().describe('Optional delay in milliseconds before rendering (max 5000)'),
|
|
93
|
+
}, async ({ html, url, format, landscape, print_background, header_html, footer_html, password, wait_for, delay_ms }) => {
|
|
94
|
+
try {
|
|
95
|
+
if (options.onUsage) {
|
|
96
|
+
await options.onUsage('render_pdf');
|
|
97
|
+
}
|
|
98
|
+
const buffer = await browserService.renderPdf({
|
|
99
|
+
html,
|
|
100
|
+
url,
|
|
101
|
+
format: format || 'A4',
|
|
102
|
+
landscape: landscape || false,
|
|
103
|
+
print_background: print_background !== false,
|
|
104
|
+
header_html,
|
|
105
|
+
footer_html,
|
|
106
|
+
password,
|
|
107
|
+
wait_for,
|
|
108
|
+
delay_ms,
|
|
109
|
+
});
|
|
110
|
+
const base64Pdf = buffer.toString('base64');
|
|
111
|
+
const sizeKb = (buffer.byteLength / 1024).toFixed(1);
|
|
112
|
+
const stored = await storageService.saveFile(buffer, {
|
|
113
|
+
mimeType: 'application/pdf',
|
|
114
|
+
extension: 'pdf',
|
|
115
|
+
filename: 'rendered.pdf',
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: 'text',
|
|
121
|
+
text: `✅ **PDF successfully generated!** (${sizeKb} KB, ${format || 'A4'}${landscape ? ', Landscape' : ''}${password ? ', Encrypted' : ''})\n\n🔗 **Direct Download Link:** [${stored.hostedUrl}](${stored.hostedUrl})\n*(Link hosted on Tailprint CDN, expires in 24 hours)*\n\nBase64 Data URI:\ndata:application/pdf;base64,${base64Pdf}`,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
return {
|
|
128
|
+
isError: true,
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: 'text',
|
|
132
|
+
text: `Error rendering PDF: ${err.message || 'Unknown error'}`,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
// Tool 3: render_image
|
|
139
|
+
server.tool('render_image', 'Renders HTML with Tailwind CSS to a high-resolution PNG or JPEG image. In compatible MCP clients (Claude Desktop, Cursor), the image displays inline directly in chat.', {
|
|
140
|
+
html: z.string().optional().describe('HTML markup to render with Tailwind CSS styling. Either html or url is required.'),
|
|
141
|
+
url: z.string().url().optional().describe('Public web URL to capture as image (e.g. https://github.com). Either html or url is required.'),
|
|
142
|
+
width: z.number().optional().default(1200).describe('Image width in pixels (default: 1200 for OG banners)'),
|
|
143
|
+
height: z.number().optional().default(630).describe('Image height in pixels (default: 630 for OG banners)'),
|
|
144
|
+
type: z.enum(['png', 'jpeg']).optional().default('png').describe('Image output format (default: png)'),
|
|
145
|
+
wait_for: z.string().optional().describe('Optional CSS selector to wait for before rendering'),
|
|
146
|
+
delay_ms: z.number().optional().describe('Optional delay in milliseconds before rendering (max 5000)'),
|
|
147
|
+
}, async ({ html, url, width, height, type, wait_for, delay_ms }) => {
|
|
148
|
+
try {
|
|
149
|
+
if (options.onUsage) {
|
|
150
|
+
await options.onUsage('render_image');
|
|
151
|
+
}
|
|
152
|
+
const imgWidth = width || 1200;
|
|
153
|
+
const imgHeight = height || 630;
|
|
154
|
+
const imgType = type || 'png';
|
|
155
|
+
const buffer = await browserService.renderImage({
|
|
156
|
+
html,
|
|
157
|
+
url,
|
|
158
|
+
width: imgWidth,
|
|
159
|
+
height: imgHeight,
|
|
160
|
+
type: imgType,
|
|
161
|
+
wait_for,
|
|
162
|
+
delay_ms,
|
|
163
|
+
});
|
|
164
|
+
const base64Img = buffer.toString('base64');
|
|
165
|
+
const sizeKb = (buffer.byteLength / 1024).toFixed(1);
|
|
166
|
+
const stored = await storageService.saveFile(buffer, {
|
|
167
|
+
mimeType: `image/${imgType}`,
|
|
168
|
+
extension: imgType,
|
|
169
|
+
filename: `rendered.${imgType}`,
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: 'image',
|
|
175
|
+
data: base64Img,
|
|
176
|
+
mimeType: `image/${imgType}`,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: 'text',
|
|
180
|
+
text: `✅ **Image successfully rendered!** (${imgWidth}x${imgHeight} ${imgType.toUpperCase()}, ${sizeKb} KB)\n\n🔗 **Direct Image Link:** [${stored.hostedUrl}](${stored.hostedUrl})\n*(Hosted on Tailprint CDN, expires in 24 hours)*\n\nBase64 Data URI:\ndata:image/${imgType};base64,${base64Img}`,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
return {
|
|
187
|
+
isError: true,
|
|
188
|
+
content: [
|
|
189
|
+
{
|
|
190
|
+
type: 'text',
|
|
191
|
+
text: `Error rendering image: ${err.message || 'Unknown error'}`,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
// Tool 4: list_templates
|
|
198
|
+
server.tool('list_templates', 'Returns the catalog of available pre-designed Tailwind CSS templates (Invoices, Certificates, Executive Reports, Resumes, Open Graph Cards, Shipping Labels, Event Passes, POS Receipts).', {}, async () => {
|
|
199
|
+
const templateList = TEMPLATES.map((t) => ({
|
|
200
|
+
id: t.id,
|
|
201
|
+
title: t.title,
|
|
202
|
+
category: t.category,
|
|
203
|
+
description: t.description,
|
|
204
|
+
format: t.format,
|
|
205
|
+
landscape: t.landscape,
|
|
206
|
+
}));
|
|
207
|
+
return {
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: 'text',
|
|
211
|
+
text: JSON.stringify(templateList, null, 2),
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
// Tool 5: get_template_source
|
|
217
|
+
server.tool('get_template_source', 'Retrieves the full Tailwind CSS HTML source code of a template by ID, allowing an AI agent to customize data and render it with render_pdf or render_image.', {
|
|
218
|
+
template_id: z.enum(TEMPLATE_IDS).describe('Template identifier'),
|
|
219
|
+
}, async ({ template_id }) => {
|
|
220
|
+
const template = TEMPLATES.find((t) => t.id === template_id);
|
|
221
|
+
if (!template) {
|
|
222
|
+
return {
|
|
223
|
+
isError: true,
|
|
224
|
+
content: [
|
|
225
|
+
{
|
|
226
|
+
type: 'text',
|
|
227
|
+
text: `Template "${template_id}" not found. Available: ${TEMPLATES.map((t) => t.id).join(', ')}`,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: 'text',
|
|
236
|
+
text: template.html,
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
// Tool 6: render_template (Data-driven direct template rendering)
|
|
242
|
+
server.tool('render_template', 'Renders a pre-designed Tailwind CSS template directly with optional variable data substitution. Ideal for generating invoices, receipts, and certificates without managing raw HTML markup.', {
|
|
243
|
+
template_id: z.enum(TEMPLATE_IDS).describe('Template identifier to render'),
|
|
244
|
+
variables: z.record(z.union([z.string(), z.number()])).optional().describe('Key-value pairs to replace in template HTML. Replaces {{key}} tags or exact text matches.'),
|
|
245
|
+
output_format: z.enum(['pdf', 'png', 'jpeg']).optional().default('pdf').describe('Output format: "pdf" for documents, "png" or "jpeg" for images (default: pdf)'),
|
|
246
|
+
landscape: z.boolean().optional().describe('Orientation override (landscape: true or false)'),
|
|
247
|
+
password: z.string().optional().describe('Optional AES-256 password to encrypt the PDF document'),
|
|
248
|
+
}, async ({ template_id, variables, output_format, landscape, password }) => {
|
|
249
|
+
try {
|
|
250
|
+
if (options.onUsage) {
|
|
251
|
+
await options.onUsage('render_template');
|
|
252
|
+
}
|
|
253
|
+
const template = TEMPLATES.find((t) => t.id === template_id);
|
|
254
|
+
if (!template) {
|
|
255
|
+
return {
|
|
256
|
+
isError: true,
|
|
257
|
+
content: [{ type: 'text', text: `Template "${template_id}" not found.` }],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
let renderedHtml = template.html;
|
|
261
|
+
if (variables) {
|
|
262
|
+
for (const [key, val] of Object.entries(variables)) {
|
|
263
|
+
const strVal = String(val);
|
|
264
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
265
|
+
const placeholderPattern = new RegExp(`\\{\\{\\s*${escapedKey}\\s*\\}\\}`, 'g');
|
|
266
|
+
if (placeholderPattern.test(renderedHtml)) {
|
|
267
|
+
renderedHtml = renderedHtml.replace(placeholderPattern, strVal);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
renderedHtml = renderedHtml.replaceAll(key, strVal);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const targetFormat = output_format || 'pdf';
|
|
275
|
+
if (targetFormat === 'pdf') {
|
|
276
|
+
const buffer = await browserService.renderPdf({
|
|
277
|
+
html: renderedHtml,
|
|
278
|
+
format: template.format || 'A4',
|
|
279
|
+
landscape: landscape !== undefined ? landscape : (template.landscape || false),
|
|
280
|
+
print_background: true,
|
|
281
|
+
password,
|
|
282
|
+
});
|
|
283
|
+
const base64Pdf = buffer.toString('base64');
|
|
284
|
+
const sizeKb = (buffer.byteLength / 1024).toFixed(1);
|
|
285
|
+
const stored = await storageService.saveFile(buffer, {
|
|
286
|
+
mimeType: 'application/pdf',
|
|
287
|
+
extension: 'pdf',
|
|
288
|
+
filename: `${template.id}.pdf`,
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
content: [
|
|
292
|
+
{
|
|
293
|
+
type: 'text',
|
|
294
|
+
text: `✅ **Template "${template.title}" rendered to PDF!** (${sizeKb} KB, ${template.format || 'A4'}${password ? ', Encrypted' : ''})\n\n🔗 **Direct Download Link:** [${stored.hostedUrl}](${stored.hostedUrl})\n*(Link hosted on Tailprint CDN, expires in 24 hours)*\n\nBase64 Data URI:\ndata:application/pdf;base64,${base64Pdf}`,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const isLandscape = landscape !== undefined ? landscape : (template.landscape || false);
|
|
301
|
+
const width = isLandscape ? 1200 : 800;
|
|
302
|
+
const height = isLandscape ? 630 : 1000;
|
|
303
|
+
const buffer = await browserService.renderImage({
|
|
304
|
+
html: renderedHtml,
|
|
305
|
+
width,
|
|
306
|
+
height,
|
|
307
|
+
type: targetFormat,
|
|
308
|
+
});
|
|
309
|
+
const base64Img = buffer.toString('base64');
|
|
310
|
+
const sizeKb = (buffer.byteLength / 1024).toFixed(1);
|
|
311
|
+
return {
|
|
312
|
+
content: [
|
|
313
|
+
{
|
|
314
|
+
type: 'image',
|
|
315
|
+
data: base64Img,
|
|
316
|
+
mimeType: `image/${targetFormat}`,
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
type: 'text',
|
|
320
|
+
text: `✅ Template "${template.title}" rendered to ${targetFormat.toUpperCase()} (${width}x${height}, ${sizeKb} KB).\n\nBase64 Data URI:\ndata:image/${targetFormat};base64,${base64Img}`,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
return {
|
|
328
|
+
isError: true,
|
|
329
|
+
content: [
|
|
330
|
+
{
|
|
331
|
+
type: 'text',
|
|
332
|
+
text: `Error rendering template "${template_id}": ${err.message || 'Unknown error'}`,
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
// Tool 7: render_chart (High-DPI Data Visualizations)
|
|
339
|
+
server.tool('render_chart', 'Renders high-DPI, interactive-grade data visualizations (Bar, Line, Pie, Doughnut, PolarArea, Radar) using Chart.js inside headless Chromium. In compatible MCP clients (Claude Desktop, Cursor), the chart displays inline directly in chat.', {
|
|
340
|
+
type: z.enum(['bar', 'line', 'pie', 'doughnut', 'radar', 'polarArea']).describe('Chart type'),
|
|
341
|
+
labels: z.array(z.string()).describe('Array of category or time labels (e.g. ["Jan", "Feb", "Mar", "Apr"])'),
|
|
342
|
+
datasets: z.array(z.object({
|
|
343
|
+
label: z.string().describe('Dataset name'),
|
|
344
|
+
data: z.array(z.number()).describe('Array of numeric values matching the labels'),
|
|
345
|
+
backgroundColor: z.union([z.string(), z.array(z.string())]).optional().describe('Color hex or array of color hexes'),
|
|
346
|
+
borderColor: z.union([z.string(), z.array(z.string())]).optional().describe('Border or line color hex'),
|
|
347
|
+
borderWidth: z.number().optional().describe('Border line width in pixels'),
|
|
348
|
+
fill: z.boolean().optional().describe('Whether to fill the area under the line (for line charts)'),
|
|
349
|
+
})).describe('Array of datasets to plot'),
|
|
350
|
+
title: z.string().optional().describe('Optional chart headline/title'),
|
|
351
|
+
theme: z.enum(['dark', 'light']).optional().default('dark').describe('Color theme (default: dark)'),
|
|
352
|
+
width: z.number().optional().default(1000).describe('Chart image width in pixels (default: 1000)'),
|
|
353
|
+
height: z.number().optional().default(600).describe('Chart image height in pixels (default: 600)'),
|
|
354
|
+
}, async ({ type, labels, datasets, title, theme, width, height }) => {
|
|
355
|
+
try {
|
|
356
|
+
if (options.onUsage) {
|
|
357
|
+
await options.onUsage('render_chart');
|
|
358
|
+
}
|
|
359
|
+
const buffer = await chartService.renderChart({
|
|
360
|
+
type,
|
|
361
|
+
data: { labels, datasets },
|
|
362
|
+
title,
|
|
363
|
+
theme: theme || 'dark',
|
|
364
|
+
width: width || 1000,
|
|
365
|
+
height: height || 600,
|
|
366
|
+
});
|
|
367
|
+
const base64Img = buffer.toString('base64');
|
|
368
|
+
const sizeKb = (buffer.byteLength / 1024).toFixed(1);
|
|
369
|
+
const stored = await storageService.saveFile(buffer, {
|
|
370
|
+
mimeType: 'image/png',
|
|
371
|
+
extension: 'png',
|
|
372
|
+
filename: 'chart.png',
|
|
373
|
+
});
|
|
374
|
+
return {
|
|
375
|
+
content: [
|
|
376
|
+
{
|
|
377
|
+
type: 'image',
|
|
378
|
+
data: base64Img,
|
|
379
|
+
mimeType: 'image/png',
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
type: 'text',
|
|
383
|
+
text: `✅ **${type.toUpperCase()} Chart successfully generated!** (${width || 1000}x${height || 600}, ${sizeKb} KB)\n\n🔗 **Direct Image Link:** [${stored.hostedUrl}](${stored.hostedUrl})\n*(Hosted on Tailprint CDN, expires in 24 hours)*\n\nBase64 Data URI:\ndata:image/png;base64,${base64Img}`,
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
return {
|
|
390
|
+
isError: true,
|
|
391
|
+
content: [
|
|
392
|
+
{
|
|
393
|
+
type: 'text',
|
|
394
|
+
text: `Error rendering chart: ${err.message || 'Unknown error'}`,
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
// Tool 8: generate_qr (Scannable QR Codes)
|
|
401
|
+
server.tool('generate_qr', 'Generates mathematically verified, high-resolution scannable QR codes for URLs, WiFi credentials, vCard contact cards, or payment links. Returns an inline image content block and base64 PNG data URI.', {
|
|
402
|
+
text: z.string().describe('Content to encode in the QR code (e.g. "https://tailprint.com", "WIFI:S:MyNetwork;T:WPA;P:Secret;;", "TEL:+4512345678")'),
|
|
403
|
+
size: z.number().optional().default(512).describe('QR code width/height in pixels (default: 512)'),
|
|
404
|
+
dark_color: z.string().optional().default('#000000').describe('Hex color for dark modules (default: "#000000")'),
|
|
405
|
+
light_color: z.string().optional().default('#ffffff').describe('Hex color for light background (default: "#ffffff")'),
|
|
406
|
+
error_correction: z.enum(['L', 'M', 'Q', 'H']).optional().default('M').describe('Error correction level (L: 7%, M: 15%, Q: 25%, H: 30%)'),
|
|
407
|
+
}, async ({ text, size, dark_color, light_color, error_correction }) => {
|
|
408
|
+
try {
|
|
409
|
+
if (options.onUsage) {
|
|
410
|
+
await options.onUsage('generate_qr');
|
|
411
|
+
}
|
|
412
|
+
const buffer = await qrService.generatePngBuffer({
|
|
413
|
+
text,
|
|
414
|
+
size: size || 512,
|
|
415
|
+
darkColor: dark_color || '#000000',
|
|
416
|
+
lightColor: light_color || '#ffffff',
|
|
417
|
+
errorCorrection: error_correction || 'M',
|
|
418
|
+
});
|
|
419
|
+
const base64Img = buffer.toString('base64');
|
|
420
|
+
const sizeKb = (buffer.byteLength / 1024).toFixed(1);
|
|
421
|
+
return {
|
|
422
|
+
content: [
|
|
423
|
+
{
|
|
424
|
+
type: 'image',
|
|
425
|
+
data: base64Img,
|
|
426
|
+
mimeType: 'image/png',
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
type: 'text',
|
|
430
|
+
text: `✅ Verified Scannable QR Code generated (${size || 512}x${size || 512}, ${sizeKb} KB) for: "${text}".\n\nBase64 Data URI:\ndata:image/png;base64,${base64Img}`,
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
return {
|
|
437
|
+
isError: true,
|
|
438
|
+
content: [
|
|
439
|
+
{
|
|
440
|
+
type: 'text',
|
|
441
|
+
text: `Error generating QR code: ${err.message || 'Unknown error'}`,
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
// Tool 9: render_device_mockup (Multi-Device Responsive Mockup)
|
|
448
|
+
server.tool('render_device_mockup', 'Captures any public website URL and renders it into a photorealistic MacBook Pro + floating iPhone multi-device presentation mockup on a studio gradient. Returns an inline image content block and base64 PNG.', {
|
|
449
|
+
url: z.string().url().describe('Public website URL to capture as responsive mockup (e.g. "https://stripe.com" or "https://tailprint.com")'),
|
|
450
|
+
title: z.string().optional().describe('Optional title label on the mockup header'),
|
|
451
|
+
delay_ms: z.number().optional().default(500).describe('Delay in milliseconds before capturing screenshots to allow fonts/animations to load (max 5000)'),
|
|
452
|
+
}, async ({ url, title, delay_ms }) => {
|
|
453
|
+
try {
|
|
454
|
+
if (options.onUsage) {
|
|
455
|
+
await options.onUsage('render_device_mockup');
|
|
456
|
+
}
|
|
457
|
+
const buffer = await mockupService.renderDeviceMockup({
|
|
458
|
+
url,
|
|
459
|
+
title,
|
|
460
|
+
delay_ms: Math.min(delay_ms || 500, 5000),
|
|
461
|
+
});
|
|
462
|
+
const base64Img = buffer.toString('base64');
|
|
463
|
+
const sizeKb = (buffer.byteLength / 1024).toFixed(1);
|
|
464
|
+
const stored = await storageService.saveFile(buffer, {
|
|
465
|
+
mimeType: 'image/png',
|
|
466
|
+
extension: 'png',
|
|
467
|
+
filename: 'device-mockup.png',
|
|
468
|
+
});
|
|
469
|
+
return {
|
|
470
|
+
content: [
|
|
471
|
+
{
|
|
472
|
+
type: 'image',
|
|
473
|
+
data: base64Img,
|
|
474
|
+
mimeType: 'image/png',
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
type: 'text',
|
|
478
|
+
text: `✅ **Responsive Device Mockup successfully rendered!** (1600x1000, ${sizeKb} KB) for "${url}".\n\n🔗 **Direct Image Link:** [${stored.hostedUrl}](${stored.hostedUrl})\n*(Hosted on Tailprint CDN, expires in 24 hours)*\n\nBase64 Data URI:\ndata:image/png;base64,${base64Img}`,
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
return {
|
|
485
|
+
isError: true,
|
|
486
|
+
content: [
|
|
487
|
+
{
|
|
488
|
+
type: 'text',
|
|
489
|
+
text: `Error rendering device mockup for "${url}": ${err.message || 'Unknown error'}`,
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
// Tool 10: stamp_pdf (PDF Watermark & Status Overlay)
|
|
496
|
+
server.tool('stamp_pdf', 'Applies a diagonal semi-transparent watermark or status stamp (e.g. "CONFIDENTIAL", "DRAFT", "FORTROLIGT", "TILBUD") across all pages of an existing vector PDF using pdf-lib.', {
|
|
497
|
+
pdf_base64: z.string().describe('Base64-encoded PDF string (e.g. output from render_pdf or render_template)'),
|
|
498
|
+
watermark_text: z.string().describe('Watermark text to stamp diagonally across each page (e.g. "CONFIDENTIAL", "DRAFT", "FORTROLIGT", "TILBUD")'),
|
|
499
|
+
opacity: z.number().optional().default(0.15).describe('Watermark opacity from 0.05 (subtle) to 0.5 (prominent) - default: 0.15'),
|
|
500
|
+
color: z.enum(['red', 'gray', 'blue']).optional().default('red').describe('Watermark color: "red", "gray", or "blue" (default: "red")'),
|
|
501
|
+
font_size: z.number().optional().default(54).describe('Font size of watermark text in points (default: 54)'),
|
|
502
|
+
}, async ({ pdf_base64, watermark_text, opacity, color, font_size }) => {
|
|
503
|
+
try {
|
|
504
|
+
if (options.onUsage) {
|
|
505
|
+
await options.onUsage('stamp_pdf');
|
|
506
|
+
}
|
|
507
|
+
// Clean base64 string if it contains data URI header
|
|
508
|
+
const cleanBase64 = pdf_base64.replace(/^data:application\/pdf;base64,/, '').trim();
|
|
509
|
+
const inputBuffer = Buffer.from(cleanBase64, 'base64');
|
|
510
|
+
const stampedBuffer = await pdfStamperService.applyWatermark({
|
|
511
|
+
pdfBuffer: inputBuffer,
|
|
512
|
+
text: watermark_text,
|
|
513
|
+
opacity: opacity || 0.15,
|
|
514
|
+
color: color || 'red',
|
|
515
|
+
fontSize: font_size || 54,
|
|
516
|
+
});
|
|
517
|
+
const outputBase64 = stampedBuffer.toString('base64');
|
|
518
|
+
const sizeKb = (stampedBuffer.byteLength / 1024).toFixed(1);
|
|
519
|
+
const stored = await storageService.saveFile(stampedBuffer, {
|
|
520
|
+
mimeType: 'application/pdf',
|
|
521
|
+
extension: 'pdf',
|
|
522
|
+
filename: 'watermarked.pdf',
|
|
523
|
+
});
|
|
524
|
+
return {
|
|
525
|
+
content: [
|
|
526
|
+
{
|
|
527
|
+
type: 'text',
|
|
528
|
+
text: `✅ **PDF successfully stamped with watermark "${watermark_text}"!** (${sizeKb} KB)\n\n🔗 **Direct Download Link:** [${stored.hostedUrl}](${stored.hostedUrl})\n*(Link hosted on Tailprint CDN, expires in 24 hours)*\n\nBase64 Data URI:\ndata:application/pdf;base64,${outputBase64}`,
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
return {
|
|
535
|
+
isError: true,
|
|
536
|
+
content: [
|
|
537
|
+
{
|
|
538
|
+
type: 'text',
|
|
539
|
+
text: `Error stamping PDF: ${err.message || 'Unknown error'}`,
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
// Tool 11: compare_snapshots (Visual Regression Diff Heatmap)
|
|
546
|
+
server.tool('compare_snapshots', 'Captures two URLs, HTML snippets, or base64 screenshots and generates a pixel-precise visual regression diff heatmap. Returns a 3-panel side-by-side studio comparison image and diff statistics.', {
|
|
547
|
+
before_url: z.string().url().optional().describe('Baseline web URL before changes'),
|
|
548
|
+
after_url: z.string().url().optional().describe('Modified web URL after changes'),
|
|
549
|
+
before_html: z.string().optional().describe('Baseline HTML markup before changes'),
|
|
550
|
+
after_html: z.string().optional().describe('Modified HTML markup after changes'),
|
|
551
|
+
before_image_base64: z.string().optional().describe('Optional baseline PNG base64 string'),
|
|
552
|
+
after_image_base64: z.string().optional().describe('Optional modified PNG base64 string'),
|
|
553
|
+
width: z.number().optional().default(1200).describe('Viewport width in pixels (default: 1200)'),
|
|
554
|
+
height: z.number().optional().default(800).describe('Viewport height in pixels (default: 800)'),
|
|
555
|
+
threshold: z.number().optional().default(0.1).describe('Matching threshold from 0.0 (strict) to 1.0 (loose) - default: 0.1'),
|
|
556
|
+
}, async ({ before_url, after_url, before_html, after_html, before_image_base64, after_image_base64, width, height, threshold }) => {
|
|
557
|
+
try {
|
|
558
|
+
if (options.onUsage) {
|
|
559
|
+
await options.onUsage('compare_snapshots');
|
|
560
|
+
}
|
|
561
|
+
const result = await visualDiffService.compareSnapshots({
|
|
562
|
+
beforeUrl: before_url,
|
|
563
|
+
afterUrl: after_url,
|
|
564
|
+
beforeHtml: before_html,
|
|
565
|
+
afterHtml: after_html,
|
|
566
|
+
beforeImageBase64: before_image_base64,
|
|
567
|
+
afterImageBase64: after_image_base64,
|
|
568
|
+
width: width || 1200,
|
|
569
|
+
height: height || 800,
|
|
570
|
+
threshold: threshold !== undefined ? threshold : 0.1,
|
|
571
|
+
});
|
|
572
|
+
const stored = await storageService.saveFile(result.compositeImageBuffer, {
|
|
573
|
+
mimeType: 'image/png',
|
|
574
|
+
extension: 'png',
|
|
575
|
+
filename: 'visual-diff.png',
|
|
576
|
+
});
|
|
577
|
+
const compositeBase64 = result.compositeImageBuffer.toString('base64');
|
|
578
|
+
const statusText = result.isIdentical
|
|
579
|
+
? 'Identical (0% difference)'
|
|
580
|
+
: `${result.diffPercentage}% layout shift (${result.differentPixels.toLocaleString()} changed pixels)`;
|
|
581
|
+
return {
|
|
582
|
+
content: [
|
|
583
|
+
{
|
|
584
|
+
type: 'image',
|
|
585
|
+
data: compositeBase64,
|
|
586
|
+
mimeType: 'image/png',
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
type: 'text',
|
|
590
|
+
text: `🔍 **Visual Regression Report Generated!**\n\n- **Status:** ${statusText}\n- **Total Pixels:** ${result.totalPixels.toLocaleString()}\n- **Changed Pixels:** ${result.differentPixels.toLocaleString()}\n- **Diff Percentage:** ${result.diffPercentage}%\n\n🔗 **Direct Studio Report Link:** [${stored.hostedUrl}](${stored.hostedUrl})\n*(Hosted on Tailprint CDN, expires in 24 hours)*`,
|
|
591
|
+
},
|
|
592
|
+
],
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
return {
|
|
597
|
+
isError: true,
|
|
598
|
+
content: [
|
|
599
|
+
{
|
|
600
|
+
type: 'text',
|
|
601
|
+
text: `Error comparing snapshots: ${err.message || 'Unknown error'}`,
|
|
602
|
+
},
|
|
603
|
+
],
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
// Tool 12: render_carousel (LinkedIn / Instagram Multi-Slide Carousel)
|
|
608
|
+
server.tool('render_carousel', 'Generates high-converting multi-slide social media carousels (1080x1350 px, 4:5 portrait) for LinkedIn (multi-page PDF) and Instagram/Twitter (PNG). Accepts markdown or structured slides.', {
|
|
609
|
+
title: z.string().optional().describe('Carousel main title / topic'),
|
|
610
|
+
subtitle: z.string().optional().describe('Optional subtitle or hook'),
|
|
611
|
+
author_name: z.string().optional().default('Tailprint Insights').describe('Author name to brand each slide'),
|
|
612
|
+
author_handle: z.string().optional().default('@tailprint').describe('Social handle (e.g. @company or @founder)'),
|
|
613
|
+
author_role: z.string().optional().describe('Author title / role (e.g. "Full-Stack Engineer" or "Tech Lead")'),
|
|
614
|
+
theme: z.enum(['dark', 'light', 'emerald', 'gradient']).optional().default('dark').describe('Visual aesthetic theme (dark, light, emerald, gradient)'),
|
|
615
|
+
markdown: z.string().optional().describe('Markdown text where slides are separated by "---" with # headlines and bullet points.'),
|
|
616
|
+
slides: z
|
|
617
|
+
.array(z.object({
|
|
618
|
+
headline: z.string().describe('Slide headline'),
|
|
619
|
+
content: z.union([z.string(), z.array(z.string())]).describe('Slide body paragraph or bullet points array'),
|
|
620
|
+
tag: z.string().optional().describe('Top tag pill (e.g. "RULE 01", "MISTAKE", "TIP")'),
|
|
621
|
+
takeaway: z.string().optional().describe('Bottom takeaway callout box'),
|
|
622
|
+
}))
|
|
623
|
+
.optional()
|
|
624
|
+
.describe('Structured slide array'),
|
|
625
|
+
}, async ({ title, subtitle, author_name, author_handle, author_role, theme, markdown, slides }) => {
|
|
626
|
+
try {
|
|
627
|
+
if (options.onUsage) {
|
|
628
|
+
await options.onUsage('render_carousel');
|
|
629
|
+
}
|
|
630
|
+
const result = await carouselService.renderCarousel({
|
|
631
|
+
title,
|
|
632
|
+
subtitle,
|
|
633
|
+
author: {
|
|
634
|
+
name: author_name || 'Tailprint Insights',
|
|
635
|
+
handle: author_handle || '@tailprint',
|
|
636
|
+
role: author_role || 'High-Performance Engine',
|
|
637
|
+
},
|
|
638
|
+
theme: theme || 'dark',
|
|
639
|
+
markdown,
|
|
640
|
+
slides,
|
|
641
|
+
});
|
|
642
|
+
const previewBase64 = result.previewImageBuffer.toString('base64');
|
|
643
|
+
return {
|
|
644
|
+
content: [
|
|
645
|
+
{
|
|
646
|
+
type: 'image',
|
|
647
|
+
data: previewBase64,
|
|
648
|
+
mimeType: 'image/png',
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
type: 'text',
|
|
652
|
+
text: `📱 **Multi-Slide Carousel Generated Successfully!** (${result.slideCount} slides, 1080x1350 px)\n\n📥 **LinkedIn Carousel (Multi-page PDF):** [${result.pdfStored?.hostedUrl}](${result.pdfStored?.hostedUrl})\n*(Upload this PDF directly to LinkedIn as a document carousel post)*\n\n🖼️ **Instagram / Cover Slide (PNG):** [${result.previewStored?.hostedUrl}](${result.previewStored?.hostedUrl})\n*(Hosted on Tailprint CDN, expires in 24 hours)*`,
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
return {
|
|
659
|
+
isError: true,
|
|
660
|
+
content: [
|
|
661
|
+
{
|
|
662
|
+
type: 'text',
|
|
663
|
+
text: `Error generating carousel: ${err.message || 'Unknown error'}`,
|
|
664
|
+
},
|
|
665
|
+
],
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
// Tool 13: generate_einvoice
|
|
670
|
+
server.tool('generate_einvoice', 'Generates a legally compliant European hybrid electronic invoice (Factur-X / ZUGFeRD 2.2 / EN 16931). Renders a beautiful visual PDF and embeds machine-readable CrossIndustryInvoice XML (/EmbeddedFiles) for automated ingestion by ERP and accounting platforms (DATEV, SAP, Chorus Pro, Dinero, Billy).', {
|
|
671
|
+
invoice_number: z.string().describe('Unique invoice reference number (e.g. "INV-2026-0042")'),
|
|
672
|
+
seller_name: z.string().describe('Supplier/seller company name'),
|
|
673
|
+
seller_vat_id: z.string().optional().describe('Supplier VAT ID / Tax number (e.g. "DK12345678" or "DE987654321")'),
|
|
674
|
+
seller_country: z.string().length(2).optional().default('DK').describe('2-letter ISO country code of seller (e.g. DK, DE, FR, NL)'),
|
|
675
|
+
seller_street: z.string().optional().describe('Seller street address'),
|
|
676
|
+
seller_city: z.string().optional().describe('Seller city'),
|
|
677
|
+
seller_postcode: z.string().optional().describe('Seller postal code'),
|
|
678
|
+
seller_email: z.string().optional().describe('Seller billing email'),
|
|
679
|
+
buyer_name: z.string().describe('Customer/buyer company or client name'),
|
|
680
|
+
buyer_vat_id: z.string().optional().describe('Buyer VAT ID / Tax number (e.g. "DE123456789")'),
|
|
681
|
+
buyer_country: z.string().length(2).optional().default('DE').describe('2-letter ISO country code of buyer (e.g. DE, FR, DK)'),
|
|
682
|
+
buyer_street: z.string().optional().describe('Buyer street address'),
|
|
683
|
+
buyer_city: z.string().optional().describe('Buyer city'),
|
|
684
|
+
buyer_postcode: z.string().optional().describe('Buyer postal code'),
|
|
685
|
+
buyer_email: z.string().optional().describe('Buyer contact/AP email'),
|
|
686
|
+
line_items: z
|
|
687
|
+
.array(z.object({
|
|
688
|
+
name: z.string().describe('Product or service description'),
|
|
689
|
+
quantity: z.number().positive().describe('Quantity or units'),
|
|
690
|
+
unit_price: z.number().nonnegative().describe('Price per unit excluding VAT'),
|
|
691
|
+
vat_rate: z.number().nonnegative().optional().default(25).describe('VAT percentage (e.g. 25 for 25% or 0 for exempt)'),
|
|
692
|
+
}))
|
|
693
|
+
.min(1)
|
|
694
|
+
.describe('List of invoice items or billed services'),
|
|
695
|
+
currency: z.string().length(3).optional().default('EUR').describe('ISO 4217 currency code (e.g. EUR, DKK, USD, SEK)'),
|
|
696
|
+
due_date: z.string().optional().describe('Due payment date (YYYY-MM-DD)'),
|
|
697
|
+
payment_iban: z.string().optional().describe('Bank transfer IBAN for direct wire payment'),
|
|
698
|
+
payment_bic: z.string().optional().describe('Bank SWIFT/BIC code'),
|
|
699
|
+
notes: z.string().optional().describe('Optional payment terms or notes'),
|
|
700
|
+
profile: z.enum(['BASIC', 'EN16931']).optional().default('BASIC').describe('Factur-X compliance profile level'),
|
|
701
|
+
}, async ({ invoice_number, seller_name, seller_vat_id, seller_country, seller_street, seller_city, seller_postcode, seller_email, buyer_name, buyer_vat_id, buyer_country, buyer_street, buyer_city, buyer_postcode, buyer_email, line_items, currency, due_date, payment_iban, payment_bic, notes, profile, }) => {
|
|
702
|
+
try {
|
|
703
|
+
if (options.onUsage) {
|
|
704
|
+
await options.onUsage('generate_einvoice');
|
|
705
|
+
}
|
|
706
|
+
const result = await eInvoiceService.generateHybridInvoice({
|
|
707
|
+
invoiceNumber: invoice_number,
|
|
708
|
+
currency: currency || 'EUR',
|
|
709
|
+
dueDate: due_date,
|
|
710
|
+
seller: {
|
|
711
|
+
name: seller_name,
|
|
712
|
+
vatId: seller_vat_id,
|
|
713
|
+
country: seller_country || 'DK',
|
|
714
|
+
street: seller_street,
|
|
715
|
+
city: seller_city,
|
|
716
|
+
postcode: seller_postcode,
|
|
717
|
+
email: seller_email,
|
|
718
|
+
},
|
|
719
|
+
buyer: {
|
|
720
|
+
name: buyer_name,
|
|
721
|
+
vatId: buyer_vat_id,
|
|
722
|
+
country: buyer_country || 'DE',
|
|
723
|
+
street: buyer_street,
|
|
724
|
+
city: buyer_city,
|
|
725
|
+
postcode: buyer_postcode,
|
|
726
|
+
email: buyer_email,
|
|
727
|
+
},
|
|
728
|
+
lineItems: line_items.map((item) => ({
|
|
729
|
+
name: item.name,
|
|
730
|
+
quantity: item.quantity,
|
|
731
|
+
unitPrice: item.unit_price,
|
|
732
|
+
vatRate: item.vat_rate,
|
|
733
|
+
})),
|
|
734
|
+
paymentIban: payment_iban,
|
|
735
|
+
paymentBic: payment_bic,
|
|
736
|
+
notes,
|
|
737
|
+
profile: profile || 'BASIC',
|
|
738
|
+
});
|
|
739
|
+
return {
|
|
740
|
+
content: [
|
|
741
|
+
{
|
|
742
|
+
type: 'text',
|
|
743
|
+
text: [
|
|
744
|
+
`🇪🇺 **Factur-X / ZUGFeRD 2.2 Compliant Hybrid E-Invoice Generated!**`,
|
|
745
|
+
``,
|
|
746
|
+
`- **Invoice Number:** \`${result.invoiceNumber}\``,
|
|
747
|
+
`- **Currency:** \`${result.currency}\``,
|
|
748
|
+
`- **Subtotal (Net):** \`${result.netTotal.toFixed(2)} ${result.currency}\``,
|
|
749
|
+
`- **Total VAT:** \`${result.taxTotal.toFixed(2)} ${result.currency}\``,
|
|
750
|
+
`- **Total Due (Gross):** \`${result.grossTotal.toFixed(2)} ${result.currency}\``,
|
|
751
|
+
`- **Standard:** EN 16931 / PEPPOL / Factur-X (${profile || 'BASIC'} Profile)`,
|
|
752
|
+
`- **Embedded Machine Data:** \`factur-x.xml\` (CrossIndustryInvoice, attached to PDF catalog)`,
|
|
753
|
+
``,
|
|
754
|
+
`📥 **Download Hybrid PDF:** [${result.pdfStored?.hostedUrl}](${result.pdfStored?.hostedUrl})`,
|
|
755
|
+
``,
|
|
756
|
+
`*Compatible with DATEV, SAP, Chorus Pro, Dinero, Billy, and all European tax authorities.*`,
|
|
757
|
+
].join('\n'),
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
return {
|
|
764
|
+
isError: true,
|
|
765
|
+
content: [
|
|
766
|
+
{
|
|
767
|
+
type: 'text',
|
|
768
|
+
text: `Error generating EU E-Invoice: ${err.message || 'Unknown error'}`,
|
|
769
|
+
},
|
|
770
|
+
],
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
// ==========================================
|
|
775
|
+
// MCP RESOURCES
|
|
776
|
+
// ==========================================
|
|
777
|
+
// Resource 1: Complete catalog of templates
|
|
778
|
+
server.resource('templates-catalog', 'tailprint://templates/catalog', { description: 'Complete catalog of all available Tailwind CSS document and graphic templates', mimeType: 'application/json' }, async (uri) => ({
|
|
779
|
+
contents: [
|
|
780
|
+
{
|
|
781
|
+
uri: uri.href,
|
|
782
|
+
mimeType: 'application/json',
|
|
783
|
+
text: JSON.stringify(TEMPLATES.map((t) => ({
|
|
784
|
+
id: t.id,
|
|
785
|
+
title: t.title,
|
|
786
|
+
category: t.category,
|
|
787
|
+
description: t.description,
|
|
788
|
+
format: t.format,
|
|
789
|
+
landscape: t.landscape,
|
|
790
|
+
})), null, 2),
|
|
791
|
+
},
|
|
792
|
+
],
|
|
793
|
+
}));
|
|
794
|
+
// Resource 2: Tailwind PDF Best Practices Guide
|
|
795
|
+
server.resource('tailwind-pdf-guide', 'tailprint://docs/tailwind-guide', { description: 'Developer guide for crafting pixel-perfect, print-ready documents with Tailwind CSS', mimeType: 'text/markdown' }, async (uri) => ({
|
|
796
|
+
contents: [
|
|
797
|
+
{
|
|
798
|
+
uri: uri.href,
|
|
799
|
+
mimeType: 'text/markdown',
|
|
800
|
+
text: [
|
|
801
|
+
'# Tailprint Guide: Crafting Beautiful Documents with Tailwind CSS',
|
|
802
|
+
'',
|
|
803
|
+
'Tailprint combines headless Chromium with full Tailwind CSS utility classes.',
|
|
804
|
+
'',
|
|
805
|
+
'## Page Sizing & Container Dimensions',
|
|
806
|
+
'- **A4 Portrait:** Set root element to `class="w-[210mm] min-h-[297mm] p-10 bg-white text-slate-800"`',
|
|
807
|
+
'- **A4 Landscape:** Set root element to `class="w-[297mm] min-h-[210mm] p-10 bg-white text-slate-800"`',
|
|
808
|
+
'- **Open Graph Social Card:** Set root element to `class="w-[1200px] h-[630px] flex flex-col justify-between p-16"`',
|
|
809
|
+
'',
|
|
810
|
+
'## Page Breaks in Multi-Page Documents',
|
|
811
|
+
'- Prevent elements from splitting across page breaks: `break-inside-avoid` (CSS: `break-inside: avoid;`)',
|
|
812
|
+
'- Force a new page break: `break-after-page` (CSS: `break-after: page;`)',
|
|
813
|
+
'',
|
|
814
|
+
'## Chromium Page Numbers in Headers & Footers',
|
|
815
|
+
'When using `header_html` or `footer_html`, Chromium automatically populates these special classes:',
|
|
816
|
+
'- `<span class="pageNumber"></span>` – Current page index',
|
|
817
|
+
'- `<span class="totalPages"></span>` – Total pages count',
|
|
818
|
+
'- `<span class="date"></span>` – Formatted render date',
|
|
819
|
+
'- `<span class="title"></span>` – Document title',
|
|
820
|
+
'',
|
|
821
|
+
'Note: Header and footer templates must include inline styles for font size (e.g. `font-size: 9px;`).',
|
|
822
|
+
].join('\n'),
|
|
823
|
+
},
|
|
824
|
+
],
|
|
825
|
+
}));
|
|
826
|
+
// Resources 3..N: Static template resources for every template
|
|
827
|
+
for (const t of TEMPLATES) {
|
|
828
|
+
server.resource(`template-${t.id}`, `tailprint://templates/${t.id}`, { description: `${t.title} (${t.category}) HTML template with Tailwind CSS`, mimeType: 'text/html' }, async (uri) => ({
|
|
829
|
+
contents: [
|
|
830
|
+
{
|
|
831
|
+
uri: uri.href,
|
|
832
|
+
mimeType: 'text/html',
|
|
833
|
+
text: t.html,
|
|
834
|
+
},
|
|
835
|
+
],
|
|
836
|
+
}));
|
|
837
|
+
}
|
|
838
|
+
// ==========================================
|
|
839
|
+
// MCP PROMPTS
|
|
840
|
+
// ==========================================
|
|
841
|
+
// Prompt 1: Generate Invoice
|
|
842
|
+
server.prompt('generate_invoice', 'Interactively generate a professional invoice PDF from client and billing details', {
|
|
843
|
+
client_name: z.string().describe('Name of client, customer, or organization'),
|
|
844
|
+
invoice_number: z.string().optional().describe('Invoice reference number (e.g. INV-2026-001)'),
|
|
845
|
+
items_description: z.string().describe('Summary of line items, quantities, and pricing'),
|
|
846
|
+
total_amount: z.string().describe('Total invoice amount with currency (e.g. $1,450.00)'),
|
|
847
|
+
}, async ({ client_name, invoice_number, items_description, total_amount }) => {
|
|
848
|
+
return {
|
|
849
|
+
messages: [
|
|
850
|
+
{
|
|
851
|
+
role: 'user',
|
|
852
|
+
content: {
|
|
853
|
+
type: 'text',
|
|
854
|
+
text: `Generate a professional invoice PDF for client "${client_name}".\n- Invoice Number: ${invoice_number || 'INV-2026-001'}\n- Line Items: ${items_description}\n- Total: ${total_amount}\n\nPlease use the Tailprint "render_template" tool with template_id "invoice", or customize the HTML template obtained from get_template_source(template_id: "invoice") and render it with render_pdf.`,
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
};
|
|
859
|
+
});
|
|
860
|
+
// Prompt 2: Summarize URL to PDF
|
|
861
|
+
server.prompt('summarize_url_to_pdf', 'Scrapes a web article and renders it into a branded PDF executive summary', {
|
|
862
|
+
url: z.string().url().describe('The web URL to scrape and summarize'),
|
|
863
|
+
title: z.string().optional().describe('Optional report title'),
|
|
864
|
+
}, async ({ url, title }) => {
|
|
865
|
+
return {
|
|
866
|
+
messages: [
|
|
867
|
+
{
|
|
868
|
+
role: 'user',
|
|
869
|
+
content: {
|
|
870
|
+
type: 'text',
|
|
871
|
+
text: `Please scrape the web page at "${url}" using Tailprint's scrape_markdown tool. Extract the core insights, key takeaways, and data points. Then format them as an executive report using the "report" template with Tailprint's render_template or render_pdf tools.${title ? ` Title: "${title}".` : ''}`,
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
};
|
|
876
|
+
});
|
|
877
|
+
// Prompt 3: Create Social Card
|
|
878
|
+
server.prompt('create_social_card', 'Generates a high-resolution Open Graph social share image (1200x630)', {
|
|
879
|
+
title: z.string().describe('Main headline or post title'),
|
|
880
|
+
subtitle: z.string().optional().describe('Author name or brief tagline'),
|
|
881
|
+
category: z.string().optional().describe('Badge or category label (e.g. "Product Update", "Engineering")'),
|
|
882
|
+
}, async ({ title, subtitle, category }) => {
|
|
883
|
+
return {
|
|
884
|
+
messages: [
|
|
885
|
+
{
|
|
886
|
+
role: 'user',
|
|
887
|
+
content: {
|
|
888
|
+
type: 'text',
|
|
889
|
+
text: `Please design a high-converting 1200x630 Open Graph banner image for:\n- Title: "${title}"\n- Subtitle: "${subtitle || ''}"\n- Category: "${category || 'Article'}"\n\nUse the Tailprint render_template tool with template_id "og-card" (output_format: "png"), or render custom Tailwind HTML using render_image.`,
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
],
|
|
893
|
+
};
|
|
894
|
+
});
|
|
895
|
+
return server;
|
|
896
|
+
}
|
|
897
|
+
//# sourceMappingURL=server.js.map
|