@formepdf/mcp 0.5.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/README.md +85 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +102 -0
- package/dist/schemas/invoice.d.ts +139 -0
- package/dist/schemas/invoice.js +80 -0
- package/dist/schemas/letter.d.ts +103 -0
- package/dist/schemas/letter.js +65 -0
- package/dist/schemas/receipt.d.ts +80 -0
- package/dist/schemas/receipt.js +52 -0
- package/dist/schemas/report.d.ts +158 -0
- package/dist/schemas/report.js +94 -0
- package/dist/schemas/shipping-label.d.ts +75 -0
- package/dist/schemas/shipping-label.js +47 -0
- package/dist/templates/index.d.ts +10 -0
- package/dist/templates/index.js +47 -0
- package/dist/templates/invoice.d.ts +2 -0
- package/dist/templates/invoice.js +14 -0
- package/dist/templates/letter.d.ts +2 -0
- package/dist/templates/letter.js +5 -0
- package/dist/templates/receipt.d.ts +2 -0
- package/dist/templates/receipt.js +9 -0
- package/dist/templates/report.d.ts +2 -0
- package/dist/templates/report.js +179 -0
- package/dist/templates/shipping-label.d.ts +2 -0
- package/dist/templates/shipping-label.js +5 -0
- package/dist/tools/get-schema.d.ts +6 -0
- package/dist/tools/get-schema.js +15 -0
- package/dist/tools/list-templates.d.ts +5 -0
- package/dist/tools/list-templates.js +8 -0
- package/dist/tools/render-custom.d.ts +4 -0
- package/dist/tools/render-custom.js +70 -0
- package/dist/tools/render-pdf.d.ts +4 -0
- package/dist/tools/render-pdf.js +20 -0
- package/dist/zod-to-json-schema.d.ts +2 -0
- package/dist/zod-to-json-schema.js +84 -0
- package/package.json +34 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Document, Page, View, Text, Svg, Table, Row, Cell, Fixed, PageBreak, StyleSheet } from '@formepdf/react';
|
|
3
|
+
const styles = StyleSheet.create({
|
|
4
|
+
sectionTitle: { fontSize: 20, fontWeight: 700, color: '#0f172a', marginBottom: 12 },
|
|
5
|
+
bodyText: { fontSize: 10, color: '#334155', lineHeight: 1.6, marginBottom: 12 },
|
|
6
|
+
introText: { fontSize: 10, color: '#334155', lineHeight: 1.6, marginBottom: 16 },
|
|
7
|
+
headerFooterText: { fontSize: 8, color: '#94a3b8' },
|
|
8
|
+
headerBar: { flexDirection: 'row', justifyContent: 'space-between', paddingBottom: 8, borderBottomWidth: 1, borderColor: '#e2e8f0', marginBottom: 16 },
|
|
9
|
+
footerBar: { flexDirection: 'row', justifyContent: 'space-between', paddingTop: 8, borderTopWidth: 1, borderColor: '#e2e8f0' },
|
|
10
|
+
tableHeaderCell: { padding: 8 },
|
|
11
|
+
tableHeaderText: { fontSize: 9, fontWeight: 700, color: '#ffffff', textAlign: 'right' },
|
|
12
|
+
tableCell: { padding: 8 },
|
|
13
|
+
tableCellText: { fontSize: 9, color: '#334155', textAlign: 'right' },
|
|
14
|
+
chartTitle: { fontSize: 10, fontWeight: 700, color: '#334155', marginBottom: 6 },
|
|
15
|
+
recCard: { flexDirection: 'row', gap: 24, marginBottom: 16, padding: 16, backgroundColor: '#f8fafc', borderRadius: 4, borderLeftWidth: 3, borderColor: '#0f172a' },
|
|
16
|
+
recBadge: { width: 24, height: 24, backgroundColor: '#0f172a', borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
|
17
|
+
recBadgeText: { fontSize: 10, fontWeight: 700, color: '#ffffff', lineHeight: 1.2 },
|
|
18
|
+
recTitle: { fontSize: 11, fontWeight: 700, color: '#0f172a', marginBottom: 4 },
|
|
19
|
+
recBody: { fontSize: 9, color: '#475569', lineHeight: 1.5 },
|
|
20
|
+
recLabel: { fontSize: 8, fontWeight: 700, color: '#64748b' },
|
|
21
|
+
recValue: { fontSize: 8, color: '#334155' },
|
|
22
|
+
});
|
|
23
|
+
// ── Chart SVG generators ─────────────────────────────────────────────
|
|
24
|
+
const CHART_COLORS = ['#3b82f6', '#0f172a', '#64748b', '#94a3b8', '#cbd5e1'];
|
|
25
|
+
function renderBarChart(tableData) {
|
|
26
|
+
const totals = tableData.map((r) => ({
|
|
27
|
+
label: r.region,
|
|
28
|
+
value: parseFloat(r.q1.replace(/[$,]/g, '')) + parseFloat(r.q2.replace(/[$,]/g, ''))
|
|
29
|
+
+ parseFloat(r.q3.replace(/[$,]/g, '')) + parseFloat(r.q4.replace(/[$,]/g, '')),
|
|
30
|
+
}));
|
|
31
|
+
const maxVal = Math.max(...totals.map(t => t.value));
|
|
32
|
+
const w = 230, h = 150;
|
|
33
|
+
const barAreaTop = 8, barAreaBottom = h - 20;
|
|
34
|
+
const barAreaH = barAreaBottom - barAreaTop;
|
|
35
|
+
const barW = Math.min(32, (w - 20) / totals.length - 8);
|
|
36
|
+
const gap = (w - barW * totals.length) / (totals.length + 1);
|
|
37
|
+
let svg = '';
|
|
38
|
+
// Grid lines
|
|
39
|
+
for (let i = 0; i <= 4; i++) {
|
|
40
|
+
const y = barAreaTop + (barAreaH / 4) * i;
|
|
41
|
+
svg += `<line x1="0" y1="${y}" x2="${w}" y2="${y}" stroke="#e2e8f0" stroke-width="0.5"/>`;
|
|
42
|
+
}
|
|
43
|
+
// Bars and labels
|
|
44
|
+
totals.forEach((t, i) => {
|
|
45
|
+
const x = gap + i * (barW + gap);
|
|
46
|
+
const barH = (t.value / maxVal) * barAreaH;
|
|
47
|
+
const y = barAreaBottom - barH;
|
|
48
|
+
svg += `<rect x="${x}" y="${y}" width="${barW}" height="${barH}" fill="${CHART_COLORS[i % CHART_COLORS.length]}" rx="2"/>`;
|
|
49
|
+
// Region label below bar
|
|
50
|
+
const labelX = x + barW / 2;
|
|
51
|
+
svg += `<rect x="${labelX - 2}" y="${barAreaBottom + 4}" width="4" height="4" fill="${CHART_COLORS[i % CHART_COLORS.length]}" rx="1"/>`;
|
|
52
|
+
});
|
|
53
|
+
return svg;
|
|
54
|
+
}
|
|
55
|
+
/** Approximate a circular arc as cubic bezier segments. */
|
|
56
|
+
function arcPath(cx, cy, r, startAngle, endAngle) {
|
|
57
|
+
let path = '';
|
|
58
|
+
const step = Math.PI / 2; // max 90 degrees per segment
|
|
59
|
+
let a1 = startAngle;
|
|
60
|
+
while (a1 < endAngle - 0.001) {
|
|
61
|
+
const a2 = Math.min(a1 + step, endAngle);
|
|
62
|
+
const alpha = a2 - a1;
|
|
63
|
+
const k = (4 / 3) * Math.tan(alpha / 4);
|
|
64
|
+
const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1);
|
|
65
|
+
const x2 = cx + r * Math.cos(a2), y2 = cy + r * Math.sin(a2);
|
|
66
|
+
const cp1x = x1 - k * r * Math.sin(a1), cp1y = y1 + k * r * Math.cos(a1);
|
|
67
|
+
const cp2x = x2 + k * r * Math.sin(a2), cp2y = y2 - k * r * Math.cos(a2);
|
|
68
|
+
if (a1 === startAngle) {
|
|
69
|
+
path += `M ${f(x1)} ${f(y1)} `;
|
|
70
|
+
}
|
|
71
|
+
path += `C ${f(cp1x)} ${f(cp1y)} ${f(cp2x)} ${f(cp2y)} ${f(x2)} ${f(y2)} `;
|
|
72
|
+
a1 = a2;
|
|
73
|
+
}
|
|
74
|
+
return path;
|
|
75
|
+
}
|
|
76
|
+
function f(n) { return n.toFixed(2); }
|
|
77
|
+
function renderDonutChart(tableData) {
|
|
78
|
+
const totals = tableData.map((r) => ({
|
|
79
|
+
label: r.region,
|
|
80
|
+
value: parseFloat(r.q1.replace(/[$,]/g, '')) + parseFloat(r.q2.replace(/[$,]/g, ''))
|
|
81
|
+
+ parseFloat(r.q3.replace(/[$,]/g, '')) + parseFloat(r.q4.replace(/[$,]/g, '')),
|
|
82
|
+
}));
|
|
83
|
+
const sum = totals.reduce((s, t) => s + t.value, 0);
|
|
84
|
+
const cx = 55, cy = 75, r = 48, innerR = 28;
|
|
85
|
+
let svg = '';
|
|
86
|
+
let angle = -Math.PI / 2; // start at top
|
|
87
|
+
totals.forEach((t, i) => {
|
|
88
|
+
const sliceAngle = (t.value / sum) * Math.PI * 2;
|
|
89
|
+
if (sliceAngle < 0.01) {
|
|
90
|
+
angle += sliceAngle;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const endAngle = angle + sliceAngle;
|
|
94
|
+
const ix2 = cx + innerR * Math.cos(endAngle), iy2 = cy + innerR * Math.sin(endAngle);
|
|
95
|
+
const innerPath = arcPathReverse(cx, cy, innerR, angle, endAngle);
|
|
96
|
+
const fullD = arcPath(cx, cy, r, angle, endAngle) + `L ${f(ix2)} ${f(iy2)} ` + innerPath + 'Z';
|
|
97
|
+
svg += `<path d="${fullD}" fill="${CHART_COLORS[i % CHART_COLORS.length]}"/>`;
|
|
98
|
+
angle = endAngle;
|
|
99
|
+
});
|
|
100
|
+
return svg;
|
|
101
|
+
}
|
|
102
|
+
/** Reverse arc: draws from endAngle back to startAngle using line-to after the first point. */
|
|
103
|
+
function arcPathReverse(cx, cy, r, startAngle, endAngle) {
|
|
104
|
+
const step = Math.PI / 2;
|
|
105
|
+
const segments = [];
|
|
106
|
+
let a = startAngle;
|
|
107
|
+
while (a < endAngle - 0.001) {
|
|
108
|
+
const a2 = Math.min(a + step, endAngle);
|
|
109
|
+
segments.push({ a1: a, a2 });
|
|
110
|
+
a = a2;
|
|
111
|
+
}
|
|
112
|
+
// Reverse and draw each segment backwards
|
|
113
|
+
let path = '';
|
|
114
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
115
|
+
const { a1, a2 } = segments[i];
|
|
116
|
+
const alpha = a2 - a1;
|
|
117
|
+
const k = (4 / 3) * Math.tan(alpha / 4);
|
|
118
|
+
const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1);
|
|
119
|
+
const x2 = cx + r * Math.cos(a2), y2 = cy + r * Math.sin(a2);
|
|
120
|
+
const cp1x = x1 - k * r * Math.sin(a1), cp1y = y1 + k * r * Math.cos(a1);
|
|
121
|
+
const cp2x = x2 + k * r * Math.sin(a2), cp2y = y2 - k * r * Math.cos(a2);
|
|
122
|
+
// Draw from (x2,y2) to (x1,y1) with swapped control points
|
|
123
|
+
path += `C ${f(cp2x)} ${f(cp2y)} ${f(cp1x)} ${f(cp1y)} ${f(x1)} ${f(y1)} `;
|
|
124
|
+
}
|
|
125
|
+
return path;
|
|
126
|
+
}
|
|
127
|
+
function renderLineChart(tableData) {
|
|
128
|
+
const quarters = ['q1', 'q2', 'q3', 'q4'];
|
|
129
|
+
const qTotals = quarters.map(q => tableData.reduce((sum, r) => sum + parseFloat(r[q].replace(/[$,]/g, '')), 0));
|
|
130
|
+
const minVal = Math.min(...qTotals) * 0.9;
|
|
131
|
+
const maxVal = Math.max(...qTotals) * 1.05;
|
|
132
|
+
const w = 484, h = 140;
|
|
133
|
+
const padL = 16, padR = 16, padT = 12, padB = 24;
|
|
134
|
+
const plotW = w - padL - padR, plotH = h - padT - padB;
|
|
135
|
+
let svg = '';
|
|
136
|
+
// Grid lines
|
|
137
|
+
for (let i = 0; i <= 4; i++) {
|
|
138
|
+
const y = padT + (plotH / 4) * i;
|
|
139
|
+
svg += `<line x1="${padL}" y1="${y}" x2="${w - padR}" y2="${y}" stroke="#e2e8f0" stroke-width="0.5"/>`;
|
|
140
|
+
}
|
|
141
|
+
// Plot points
|
|
142
|
+
const points = qTotals.map((v, i) => ({
|
|
143
|
+
x: padL + (plotW / (quarters.length - 1)) * i,
|
|
144
|
+
y: padT + plotH - ((v - minVal) / (maxVal - minVal)) * plotH,
|
|
145
|
+
}));
|
|
146
|
+
// Fill area under the line
|
|
147
|
+
const areaPath = `M ${f(points[0].x)} ${f(padT + plotH)} L ${points.map(p => `${f(p.x)} ${f(p.y)}`).join(' L ')} L ${f(points[points.length - 1].x)} ${f(padT + plotH)} Z`;
|
|
148
|
+
svg += `<path d="${areaPath}" fill="#3b82f6" fill-opacity="0.08"/>`;
|
|
149
|
+
// Line
|
|
150
|
+
const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${f(p.x)} ${f(p.y)}`).join(' ');
|
|
151
|
+
svg += `<path d="${linePath}" fill="none" stroke="#3b82f6" stroke-width="2"/>`;
|
|
152
|
+
// Dots
|
|
153
|
+
points.forEach(p => {
|
|
154
|
+
svg += `<circle cx="${f(p.x)}" cy="${f(p.y)}" r="3.5" fill="#ffffff" stroke="#3b82f6" stroke-width="2"/>`;
|
|
155
|
+
});
|
|
156
|
+
// Quarter markers on x-axis
|
|
157
|
+
points.forEach((p) => {
|
|
158
|
+
svg += `<line x1="${f(p.x)}" y1="${f(padT + plotH)}" x2="${f(p.x)}" y2="${f(padT + plotH + 4)}" stroke="#94a3b8" stroke-width="1"/>`;
|
|
159
|
+
});
|
|
160
|
+
return svg;
|
|
161
|
+
}
|
|
162
|
+
// ── Template ─────────────────────────────────────────────────────────
|
|
163
|
+
export default function Report(data) {
|
|
164
|
+
const tableData = data.sections[1].tableData || [];
|
|
165
|
+
return (_jsxs(Document, { title: data.title, author: data.author, children: [_jsx(Page, { size: "Letter", margin: 72, children: _jsxs(View, { style: { flexGrow: 1, justifyContent: 'center' }, children: [_jsxs(View, { style: { backgroundColor: '#0f172a', padding: 32, borderRadius: 4, marginBottom: 32 }, children: [_jsx(Text, { style: { fontSize: 32, fontWeight: 700, color: '#ffffff' }, children: data.title }), _jsx(Text, { style: { fontSize: 14, color: '#94a3b8', marginTop: 12 }, children: data.subtitle })] }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }, children: [_jsxs(View, { children: [_jsx(Text, { style: { fontSize: 10, color: '#64748b' }, children: "Prepared by" }), _jsx(Text, { style: { fontSize: 12, fontWeight: 700, color: '#1e293b', marginTop: 4 }, children: data.author }), _jsx(Text, { style: { fontSize: 10, color: '#64748b', marginTop: 2 }, children: data.department })] }), _jsxs(View, { style: { alignItems: 'flex-end' }, children: [_jsx(Text, { style: { fontSize: 10, color: '#64748b' }, children: "Date" }), _jsx(Text, { style: { fontSize: 12, fontWeight: 700, color: '#1e293b', marginTop: 4 }, children: data.date }), _jsx(Text, { style: { fontSize: 10, color: '#64748b', marginTop: 2 }, children: data.classification })] })] })] }) }), _jsxs(Page, { size: "Letter", margin: 54, children: [_jsx(Fixed, { position: "header", children: _jsxs(View, { style: styles.headerBar, children: [_jsx(Text, { style: styles.headerFooterText, children: data.company }), _jsx(Text, { style: styles.headerFooterText, children: data.title })] }) }), _jsx(Fixed, { position: "footer", children: _jsxs(View, { style: styles.footerBar, children: [_jsx(Text, { style: styles.headerFooterText, children: data.classification }), _jsxs(Text, { style: styles.headerFooterText, children: ["Page ", '{{pageNumber}}', " of ", '{{totalPages}}'] })] }) }), _jsx(Text, { style: styles.sectionTitle, children: "Table of Contents" }), data.sections.map((section, i) => (_jsx(View, { href: `#${section.title}`, style: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 6, borderBottomWidth: 1, borderColor: '#f1f5f9' }, children: _jsxs(Text, { style: { fontSize: 10, color: '#2563eb', textDecoration: 'underline' }, children: [i + 1, ". ", section.title] }) }, i))), _jsx(PageBreak, {}), _jsxs(Text, { bookmark: data.sections[0].title, style: styles.sectionTitle, children: ["1. ", data.sections[0].title] }), (data.sections[0].paragraphs || []).map((p, i) => (_jsx(Text, { style: styles.bodyText, children: p }, i))), data.keyMetrics && (_jsx(View, { style: { flexDirection: 'row', gap: 12, marginTop: 8, marginBottom: 24 }, children: data.keyMetrics.map((metric, i) => (_jsxs(View, { style: { flexGrow: 1, padding: 16, backgroundColor: '#f8fafc', borderRadius: 4, borderWidth: 1, borderColor: '#e2e8f0' }, children: [_jsx(Text, { style: { fontSize: 20, fontWeight: 700, color: '#0f172a' }, children: metric.value }), _jsx(Text, { style: { fontSize: 9, color: '#64748b', marginTop: 4 }, children: metric.label })] }, i))) })), _jsx(PageBreak, {}), _jsxs(Text, { bookmark: data.sections[1].title, style: styles.sectionTitle, children: ["2. ", data.sections[1].title] }), _jsx(Text, { style: styles.introText, children: data.sections[1].intro }), _jsxs(Table, { columns: [
|
|
166
|
+
{ width: { fraction: 0.28 } },
|
|
167
|
+
{ width: { fraction: 0.18 } },
|
|
168
|
+
{ width: { fraction: 0.18 } },
|
|
169
|
+
{ width: { fraction: 0.18 } },
|
|
170
|
+
{ width: { fraction: 0.18 } }
|
|
171
|
+
], children: [_jsxs(Row, { header: true, style: { backgroundColor: '#0f172a' }, children: [_jsx(Cell, { style: styles.tableHeaderCell, children: _jsx(Text, { style: { fontSize: 9, fontWeight: 700, color: '#ffffff' }, children: "Region" }) }), _jsx(Cell, { style: styles.tableHeaderCell, children: _jsx(Text, { style: styles.tableHeaderText, children: "Q1" }) }), _jsx(Cell, { style: styles.tableHeaderCell, children: _jsx(Text, { style: styles.tableHeaderText, children: "Q2" }) }), _jsx(Cell, { style: styles.tableHeaderCell, children: _jsx(Text, { style: styles.tableHeaderText, children: "Q3" }) }), _jsx(Cell, { style: styles.tableHeaderCell, children: _jsx(Text, { style: styles.tableHeaderText, children: "Q4" }) })] }), tableData.map((row, i) => (_jsxs(Row, { style: { backgroundColor: i % 2 === 0 ? '#ffffff' : '#f8fafc' }, children: [_jsx(Cell, { style: styles.tableCell, children: _jsx(Text, { style: { fontSize: 9, color: '#334155', fontWeight: 700 }, children: row.region }) }), _jsx(Cell, { style: styles.tableCell, children: _jsx(Text, { style: styles.tableCellText, children: row.q1 }) }), _jsx(Cell, { style: styles.tableCell, children: _jsx(Text, { style: styles.tableCellText, children: row.q2 }) }), _jsx(Cell, { style: styles.tableCell, children: _jsx(Text, { style: styles.tableCellText, children: row.q3 }) }), _jsx(Cell, { style: styles.tableCell, children: _jsx(Text, { style: styles.tableCellText, children: row.q4 }) })] }, i)))] }), _jsx(PageBreak, {}), _jsxs(Text, { bookmark: data.sections[2].title, style: styles.sectionTitle, children: ["3. ", data.sections[2].title] }), _jsx(Text, { style: styles.introText, children: data.sections[2].intro }), _jsxs(View, { style: { flexDirection: 'row', gap: 16, marginBottom: 24 }, children: [_jsxs(View, { style: { flexGrow: 1 }, children: [_jsx(Text, { style: styles.chartTitle, children: "Revenue by Region" }), _jsxs(View, { style: { backgroundColor: '#f8fafc', borderRadius: 4, borderWidth: 1, borderColor: '#e2e8f0', padding: 8 }, children: [_jsx(Svg, { width: 230, height: 150, viewBox: "0 0 230 150", content: renderBarChart(tableData) }), _jsx(View, { style: { gap: 3, marginTop: 8 }, children: tableData.map((row, i) => (_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 4 }, children: [_jsx(View, { style: { width: 8, height: 8, borderRadius: 2, backgroundColor: CHART_COLORS[i % CHART_COLORS.length] } }), _jsx(Text, { style: { fontSize: 7, color: '#64748b' }, children: row.region })] }, i))) })] })] }), _jsxs(View, { style: { flexGrow: 1 }, children: [_jsx(Text, { style: styles.chartTitle, children: "Market Share" }), _jsxs(View, { style: { backgroundColor: '#f8fafc', borderRadius: 4, borderWidth: 1, borderColor: '#e2e8f0', padding: 8 }, children: [_jsx(View, { style: { alignItems: 'center' }, children: _jsx(Svg, { width: 110, height: 150, viewBox: "0 0 110 150", content: renderDonutChart(tableData) }) }), _jsx(View, { style: { gap: 3, marginTop: 8 }, children: tableData.map((row, i) => {
|
|
172
|
+
const total = tableData.reduce((s, r) => s + parseFloat(r.q1.replace(/[$,]/g, '')) + parseFloat(r.q2.replace(/[$,]/g, ''))
|
|
173
|
+
+ parseFloat(r.q3.replace(/[$,]/g, '')) + parseFloat(r.q4.replace(/[$,]/g, '')), 0);
|
|
174
|
+
const rowTotal = parseFloat(row.q1.replace(/[$,]/g, '')) + parseFloat(row.q2.replace(/[$,]/g, ''))
|
|
175
|
+
+ parseFloat(row.q3.replace(/[$,]/g, '')) + parseFloat(row.q4.replace(/[$,]/g, ''));
|
|
176
|
+
const pct = ((rowTotal / total) * 100).toFixed(0);
|
|
177
|
+
return (_jsxs(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 3 }, children: [_jsx(View, { style: { width: 8, height: 8, borderRadius: 2, backgroundColor: CHART_COLORS[i % CHART_COLORS.length] } }), _jsxs(Text, { style: { fontSize: 7, color: '#64748b' }, children: [row.region, " ", pct, "%"] })] }, i));
|
|
178
|
+
}) })] })] })] }), _jsx(Text, { style: styles.chartTitle, children: "Quarterly Growth Trend" }), _jsxs(View, { style: { backgroundColor: '#f8fafc', borderRadius: 4, borderWidth: 1, borderColor: '#e2e8f0', paddingVertical: 12, marginBottom: 24 }, children: [_jsx(Svg, { width: 484, height: 140, viewBox: "0 0 484 140", content: renderLineChart(tableData) }), _jsx(View, { style: { position: 'relative', height: 14, marginTop: 4 }, children: ['Q1', 'Q2', 'Q3', 'Q4'].map((q, i) => (_jsx(Text, { style: { position: 'absolute', left: 16 + (452 / 3) * i - 12, width: 24, fontSize: 8, color: '#64748b', textAlign: 'center' }, children: q }, i))) })] }), _jsx(PageBreak, {}), _jsxs(Text, { bookmark: data.sections[3].title, style: styles.sectionTitle, children: ["4. ", data.sections[3].title] }), _jsx(Text, { style: styles.introText, children: data.sections[3].intro }), (data.sections[3].items || []).map((item, i) => (_jsxs(View, { style: styles.recCard, children: [_jsx(View, { style: styles.recBadge, children: _jsx(Text, { style: styles.recBadgeText, children: i + 1 }) }), _jsxs(View, { style: { flexGrow: 1, flexShrink: 1 }, children: [_jsx(Text, { style: styles.recTitle, children: item.title }), _jsx(Text, { style: styles.recBody, children: item.description }), _jsxs(View, { style: { flexDirection: 'row', gap: 16, marginTop: 8 }, children: [_jsxs(View, { style: { flexDirection: 'row', gap: 4 }, children: [_jsx(Text, { style: styles.recLabel, children: "Priority:" }), _jsx(Text, { style: styles.recValue, children: item.priority })] }), _jsxs(View, { style: { flexDirection: 'row', gap: 4 }, children: [_jsx(Text, { style: styles.recLabel, children: "Timeline:" }), _jsx(Text, { style: styles.recValue, children: item.timeline })] })] })] })] }, i)))] })] }));
|
|
179
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Document, Page, View, Text } from '@formepdf/react';
|
|
3
|
+
export default function ShippingLabel(data) {
|
|
4
|
+
return (_jsx(Document, { title: `Shipping Label - ${data.tracking}`, children: _jsxs(Page, { size: { width: 288, height: 432 }, margin: 16, children: [_jsxs(View, { style: { marginBottom: 12, padding: 8 }, children: [_jsx(Text, { style: { fontSize: 7, fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 4 }, children: "From" }), _jsx(Text, { style: { fontSize: 8, color: '#334155' }, children: data.from.name }), _jsx(Text, { style: { fontSize: 8, color: '#334155' }, children: data.from.address }), _jsx(Text, { style: { fontSize: 8, color: '#334155' }, children: data.from.cityStateZip })] }), _jsx(View, { style: { borderTopWidth: 2, borderColor: '#0f172a', marginBottom: 12 } }), _jsxs(View, { style: { padding: 12, marginBottom: 12 }, children: [_jsx(Text, { style: { fontSize: 7, fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8 }, children: "To" }), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#0f172a' }, children: data.to.name }), _jsx(Text, { style: { fontSize: 10, color: '#0f172a', marginTop: 4 }, children: data.to.address }), data.to.address2 && (_jsx(Text, { style: { fontSize: 10, color: '#0f172a' }, children: data.to.address2 })), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#0f172a', marginTop: 2 }, children: data.to.cityStateZip })] }), _jsxs(View, { style: { marginBottom: 8, padding: 8 }, children: [_jsx(View, { style: { alignItems: 'center', marginBottom: 8 }, children: _jsxs(View, { style: { flexDirection: 'row', gap: 2 }, children: [_jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 2, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 1, height: 40, backgroundColor: '#0f172a' } }), _jsx(View, { style: { width: 3, height: 40, backgroundColor: '#0f172a' } })] }) }), _jsx(Text, { style: { fontSize: 8, fontWeight: 700, color: '#0f172a', letterSpacing: 2, textAlign: 'center' }, children: data.tracking })] }), _jsx(View, { style: { borderTopWidth: 1, borderColor: '#cbd5e1', marginBottom: 8 } }), _jsxs(View, { style: { flexDirection: 'row', justifyContent: 'space-between', padding: 8 }, children: [_jsxs(View, { style: { flex: 1 }, children: [_jsx(Text, { style: { fontSize: 7, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1 }, children: "Weight" }), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#0f172a', marginTop: 2 }, children: data.weight })] }), _jsxs(View, { style: { flex: 2, justifyContent: 'center', alignItems: 'center' }, children: [_jsx(Text, { style: { width: '100%', textAlign: 'center', fontSize: 7, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1 }, children: "Dimensions" }), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#0f172a', marginTop: 2 }, children: data.dimensions })] }), _jsxs(View, { style: { flex: 1, justifyContent: 'center', alignItems: 'flex-end' }, children: [_jsx(Text, { style: { width: '100%', textAlign: 'right', fontSize: 7, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1 }, children: "Service" }), _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#0f172a', marginTop: 2 }, children: data.service })] })] }), data.stamps && data.stamps.length > 0 && (_jsx(View, { style: { flexDirection: 'row', gap: 8, marginTop: 8, padding: 8 }, children: data.stamps.map((stamp, i) => (_jsx(View, { style: { flex: 1, textAlign: 'center', paddingVertical: 6, paddingHorizontal: 12, borderWidth: 2, borderColor: '#dc2626', borderRadius: 2 }, children: _jsx(Text, { style: { fontSize: 10, fontWeight: 700, color: '#dc2626', textTransform: 'uppercase' }, children: stamp }) }, i))) }))] }) }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { templates } from '../templates/index.js';
|
|
2
|
+
import { zodToJsonSchema } from '../zod-to-json-schema.js';
|
|
3
|
+
export function getSchema(templateName) {
|
|
4
|
+
const entry = templates[templateName];
|
|
5
|
+
if (!entry) {
|
|
6
|
+
const available = Object.keys(templates).join(', ');
|
|
7
|
+
throw new Error(`Template "${templateName}" not found. Available templates: ${available}`);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
name: templateName,
|
|
11
|
+
description: entry.description,
|
|
12
|
+
schema: zodToJsonSchema(entry.schema),
|
|
13
|
+
example: entry.example,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as FormeReact from '@formepdf/react';
|
|
5
|
+
export async function renderCustom(jsx, output) {
|
|
6
|
+
// Lazy-import esbuild and core to avoid startup side effects with stdio
|
|
7
|
+
const [{ transform }, { renderDocument }] = await Promise.all([
|
|
8
|
+
import('esbuild'),
|
|
9
|
+
import('@formepdf/core'),
|
|
10
|
+
]);
|
|
11
|
+
// Transpile JSX to JS
|
|
12
|
+
const result = await transform(jsx, {
|
|
13
|
+
loader: 'tsx',
|
|
14
|
+
jsx: 'transform',
|
|
15
|
+
jsxFactory: 'React.createElement',
|
|
16
|
+
jsxFragment: 'React.Fragment',
|
|
17
|
+
});
|
|
18
|
+
// Build the function body: provide all Forme components + React in scope
|
|
19
|
+
const componentNames = [
|
|
20
|
+
'Document', 'Page', 'View', 'Text', 'Image',
|
|
21
|
+
'Table', 'Row', 'Cell', 'Fixed', 'Svg', 'PageBreak',
|
|
22
|
+
'StyleSheet', 'Font',
|
|
23
|
+
];
|
|
24
|
+
const preamble = componentNames
|
|
25
|
+
.map(name => `const ${name} = FormeReact.${name};`)
|
|
26
|
+
.join('\n');
|
|
27
|
+
const code = `${preamble}\n${result.code}`;
|
|
28
|
+
// Try evaluating as a bare JSX expression first (most common case).
|
|
29
|
+
// Strip trailing semicolons/whitespace so `return (expr)` works.
|
|
30
|
+
const trimmedCode = result.code.replace(/;\s*$/, '').trim();
|
|
31
|
+
let element = null;
|
|
32
|
+
try {
|
|
33
|
+
const exprFn = new Function('React', ...componentNames, `
|
|
34
|
+
return (${trimmedCode});
|
|
35
|
+
`);
|
|
36
|
+
element = exprFn(React, ...componentNames.map(n => FormeReact[n]));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Not a bare expression — try as a script with named exports
|
|
40
|
+
}
|
|
41
|
+
// If bare expression didn't work, try as a script defining Template/App
|
|
42
|
+
if (!element || !React.isValidElement(element)) {
|
|
43
|
+
try {
|
|
44
|
+
const fn = new Function('React', 'FormeReact', `
|
|
45
|
+
${code}
|
|
46
|
+
if (typeof Template !== 'undefined') return Template;
|
|
47
|
+
if (typeof App !== 'undefined') return App;
|
|
48
|
+
return null;
|
|
49
|
+
`);
|
|
50
|
+
element = fn(React, FormeReact);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Fall through
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// If we got a function, call it to get the element
|
|
57
|
+
if (typeof element === 'function') {
|
|
58
|
+
element = element({});
|
|
59
|
+
}
|
|
60
|
+
if (!element || !React.isValidElement(element)) {
|
|
61
|
+
throw new Error('Could not find a React element to render. Your JSX should either:\n' +
|
|
62
|
+
'- Be a single JSX expression (e.g. <Document>...</Document>)\n' +
|
|
63
|
+
'- Define a function called Template or App that returns JSX\n' +
|
|
64
|
+
'- Export a default function');
|
|
65
|
+
}
|
|
66
|
+
const pdfBytes = await renderDocument(element);
|
|
67
|
+
const outputPath = resolve(output || './custom.pdf');
|
|
68
|
+
await writeFile(outputPath, pdfBytes);
|
|
69
|
+
return { path: outputPath, size: pdfBytes.length };
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { renderDocument } from '@formepdf/core';
|
|
4
|
+
import { templates } from '../templates/index.js';
|
|
5
|
+
export async function renderPdf(templateName, data, output) {
|
|
6
|
+
const entry = templates[templateName];
|
|
7
|
+
if (!entry) {
|
|
8
|
+
const available = Object.keys(templates).join(', ');
|
|
9
|
+
throw new Error(`Template "${templateName}" not found. Available templates: ${available}`);
|
|
10
|
+
}
|
|
11
|
+
// Validate data against schema
|
|
12
|
+
const parsed = entry.schema.parse(data);
|
|
13
|
+
// Render template to React element, then to PDF
|
|
14
|
+
const element = entry.fn(parsed);
|
|
15
|
+
const pdfBytes = await renderDocument(element);
|
|
16
|
+
// Write to disk
|
|
17
|
+
const outputPath = resolve(output || `./${templateName}.pdf`);
|
|
18
|
+
await writeFile(outputPath, pdfBytes);
|
|
19
|
+
return { path: outputPath, size: pdfBytes.length };
|
|
20
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/// Converts a Zod schema to a JSON Schema object.
|
|
2
|
+
/// Handles the subset of Zod types used in our template schemas.
|
|
3
|
+
export function zodToJsonSchema(schema) {
|
|
4
|
+
return convert(schema);
|
|
5
|
+
}
|
|
6
|
+
function convert(schema) {
|
|
7
|
+
const def = schema._def;
|
|
8
|
+
if (!def)
|
|
9
|
+
return {};
|
|
10
|
+
const typeName = def.typeName;
|
|
11
|
+
switch (typeName) {
|
|
12
|
+
case 'ZodString': {
|
|
13
|
+
const result = { type: 'string' };
|
|
14
|
+
if (def.description)
|
|
15
|
+
result.description = def.description;
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
case 'ZodNumber': {
|
|
19
|
+
const result = { type: 'number' };
|
|
20
|
+
if (def.description)
|
|
21
|
+
result.description = def.description;
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
case 'ZodBoolean': {
|
|
25
|
+
const result = { type: 'boolean' };
|
|
26
|
+
if (def.description)
|
|
27
|
+
result.description = def.description;
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
case 'ZodArray': {
|
|
31
|
+
const result = {
|
|
32
|
+
type: 'array',
|
|
33
|
+
items: convert(def.type),
|
|
34
|
+
};
|
|
35
|
+
if (def.description)
|
|
36
|
+
result.description = def.description;
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
case 'ZodObject': {
|
|
40
|
+
const shape = def.shape();
|
|
41
|
+
const properties = {};
|
|
42
|
+
const required = [];
|
|
43
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
44
|
+
properties[key] = convert(value);
|
|
45
|
+
// Check if the field is optional
|
|
46
|
+
if (!isOptional(value)) {
|
|
47
|
+
required.push(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const result = {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties,
|
|
53
|
+
};
|
|
54
|
+
if (required.length > 0)
|
|
55
|
+
result.required = required;
|
|
56
|
+
if (def.description)
|
|
57
|
+
result.description = def.description;
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
case 'ZodOptional': {
|
|
61
|
+
const inner = convert(def.innerType);
|
|
62
|
+
return inner;
|
|
63
|
+
}
|
|
64
|
+
case 'ZodRecord': {
|
|
65
|
+
const result = {
|
|
66
|
+
type: 'object',
|
|
67
|
+
additionalProperties: convert(def.valueType),
|
|
68
|
+
};
|
|
69
|
+
if (def.description)
|
|
70
|
+
result.description = def.description;
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
case 'ZodUnknown':
|
|
74
|
+
case 'ZodAny': {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
default:
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function isOptional(schema) {
|
|
82
|
+
const typeName = schema._def?.typeName;
|
|
83
|
+
return typeName === 'ZodOptional' || typeName === 'ZodNullable';
|
|
84
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@formepdf/mcp",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "MCP server for Forme PDF rendering — generate PDFs from AI tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"forme-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
18
|
+
"@formepdf/react": "0.5.0",
|
|
19
|
+
"@formepdf/core": "0.5.0",
|
|
20
|
+
"esbuild": "^0.24.0",
|
|
21
|
+
"zod": "^3.24.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.0.0",
|
|
25
|
+
"react": "^19.0.0",
|
|
26
|
+
"typescript": "^5.7.0"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/formepdf/forme",
|
|
32
|
+
"directory": "packages/mcp"
|
|
33
|
+
}
|
|
34
|
+
}
|