pdfx-cli 0.0.1 → 0.4.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 +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4429 -0
- package/dist/mcp/index.d.ts +38 -0
- package/dist/mcp/index.js +1261 -0
- package/package.json +60 -12
- package/README.md +0 -10
- package/index.js +0 -4
|
@@ -0,0 +1,1261 @@
|
|
|
1
|
+
// src/mcp/index.ts
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { z as z8 } from "zod";
|
|
5
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
6
|
+
|
|
7
|
+
// src/mcp/tools/add-command.ts
|
|
8
|
+
import dedent from "dedent";
|
|
9
|
+
import { z as z2 } from "zod";
|
|
10
|
+
|
|
11
|
+
// ../shared/src/schemas.ts
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
var colorTokensSchema = z.object({
|
|
14
|
+
foreground: z.string().min(1),
|
|
15
|
+
background: z.string().min(1),
|
|
16
|
+
muted: z.string().min(1),
|
|
17
|
+
mutedForeground: z.string().min(1),
|
|
18
|
+
primary: z.string().min(1),
|
|
19
|
+
primaryForeground: z.string().min(1),
|
|
20
|
+
border: z.string().min(1),
|
|
21
|
+
accent: z.string().min(1),
|
|
22
|
+
destructive: z.string().min(1),
|
|
23
|
+
success: z.string().min(1),
|
|
24
|
+
warning: z.string().min(1),
|
|
25
|
+
info: z.string().min(1)
|
|
26
|
+
});
|
|
27
|
+
var headingFontSizeSchema = z.object({
|
|
28
|
+
h1: z.number().positive(),
|
|
29
|
+
h2: z.number().positive(),
|
|
30
|
+
h3: z.number().positive(),
|
|
31
|
+
h4: z.number().positive(),
|
|
32
|
+
h5: z.number().positive(),
|
|
33
|
+
h6: z.number().positive()
|
|
34
|
+
});
|
|
35
|
+
var typographyTokensSchema = z.object({
|
|
36
|
+
body: z.object({
|
|
37
|
+
fontFamily: z.string().min(1),
|
|
38
|
+
fontSize: z.number().positive(),
|
|
39
|
+
lineHeight: z.number().positive()
|
|
40
|
+
}),
|
|
41
|
+
heading: z.object({
|
|
42
|
+
fontFamily: z.string().min(1),
|
|
43
|
+
fontWeight: z.number().int().min(100).max(900),
|
|
44
|
+
lineHeight: z.number().positive(),
|
|
45
|
+
fontSize: headingFontSizeSchema
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
var spacingTokensSchema = z.object({
|
|
49
|
+
page: z.object({
|
|
50
|
+
marginTop: z.number().min(0),
|
|
51
|
+
marginRight: z.number().min(0),
|
|
52
|
+
marginBottom: z.number().min(0),
|
|
53
|
+
marginLeft: z.number().min(0)
|
|
54
|
+
}),
|
|
55
|
+
sectionGap: z.number().min(0),
|
|
56
|
+
paragraphGap: z.number().min(0),
|
|
57
|
+
componentGap: z.number().min(0)
|
|
58
|
+
});
|
|
59
|
+
var pageTokensSchema = z.object({
|
|
60
|
+
size: z.enum(["A4", "LETTER", "LEGAL"]),
|
|
61
|
+
orientation: z.enum(["portrait", "landscape"])
|
|
62
|
+
});
|
|
63
|
+
var primitiveTokensSchema = z.object({
|
|
64
|
+
typography: z.record(z.number()),
|
|
65
|
+
spacing: z.record(z.number()),
|
|
66
|
+
fontWeights: z.object({
|
|
67
|
+
regular: z.number(),
|
|
68
|
+
medium: z.number(),
|
|
69
|
+
semibold: z.number(),
|
|
70
|
+
bold: z.number()
|
|
71
|
+
}),
|
|
72
|
+
lineHeights: z.object({
|
|
73
|
+
tight: z.number(),
|
|
74
|
+
normal: z.number(),
|
|
75
|
+
relaxed: z.number()
|
|
76
|
+
}),
|
|
77
|
+
borderRadius: z.object({
|
|
78
|
+
none: z.number(),
|
|
79
|
+
sm: z.number(),
|
|
80
|
+
md: z.number(),
|
|
81
|
+
lg: z.number(),
|
|
82
|
+
full: z.number()
|
|
83
|
+
}),
|
|
84
|
+
letterSpacing: z.object({
|
|
85
|
+
tight: z.number(),
|
|
86
|
+
normal: z.number(),
|
|
87
|
+
wide: z.number(),
|
|
88
|
+
wider: z.number()
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
var themeSchema = z.object({
|
|
92
|
+
name: z.string().min(1),
|
|
93
|
+
primitives: primitiveTokensSchema,
|
|
94
|
+
colors: colorTokensSchema,
|
|
95
|
+
typography: typographyTokensSchema,
|
|
96
|
+
spacing: spacingTokensSchema,
|
|
97
|
+
page: pageTokensSchema
|
|
98
|
+
});
|
|
99
|
+
var configSchema = z.object({
|
|
100
|
+
$schema: z.string().optional(),
|
|
101
|
+
componentDir: z.string().min(1, "componentDir must not be empty"),
|
|
102
|
+
registry: z.string().url("registry must be a valid URL"),
|
|
103
|
+
theme: z.string().min(1).optional(),
|
|
104
|
+
blockDir: z.string().min(1).optional()
|
|
105
|
+
});
|
|
106
|
+
var registryFileTypes = [
|
|
107
|
+
"registry:component",
|
|
108
|
+
"registry:lib",
|
|
109
|
+
"registry:style",
|
|
110
|
+
"registry:template",
|
|
111
|
+
"registry:block",
|
|
112
|
+
"registry:file"
|
|
113
|
+
];
|
|
114
|
+
var registryFileSchema = z.object({
|
|
115
|
+
path: z.string().min(1),
|
|
116
|
+
content: z.string(),
|
|
117
|
+
type: z.enum(registryFileTypes)
|
|
118
|
+
});
|
|
119
|
+
var registryItemSchema = z.object({
|
|
120
|
+
name: z.string().min(1),
|
|
121
|
+
type: z.string().optional(),
|
|
122
|
+
title: z.string().optional(),
|
|
123
|
+
description: z.string().optional(),
|
|
124
|
+
files: z.array(registryFileSchema).min(1, "Component must have at least one file"),
|
|
125
|
+
dependencies: z.array(z.string()).optional(),
|
|
126
|
+
devDependencies: z.array(z.string()).optional(),
|
|
127
|
+
registryDependencies: z.array(z.string()).optional(),
|
|
128
|
+
peerComponents: z.array(z.string()).optional()
|
|
129
|
+
});
|
|
130
|
+
var registryIndexItemSchema = z.object({
|
|
131
|
+
name: z.string().min(1),
|
|
132
|
+
type: z.string(),
|
|
133
|
+
title: z.string(),
|
|
134
|
+
description: z.string(),
|
|
135
|
+
files: z.array(
|
|
136
|
+
z.object({
|
|
137
|
+
path: z.string().min(1),
|
|
138
|
+
type: z.string()
|
|
139
|
+
})
|
|
140
|
+
),
|
|
141
|
+
dependencies: z.array(z.string()).optional(),
|
|
142
|
+
devDependencies: z.array(z.string()).optional(),
|
|
143
|
+
registryDependencies: z.array(z.string()).optional(),
|
|
144
|
+
peerComponents: z.array(z.string()).optional()
|
|
145
|
+
});
|
|
146
|
+
var registrySchema = z.object({
|
|
147
|
+
$schema: z.string(),
|
|
148
|
+
name: z.string(),
|
|
149
|
+
homepage: z.string(),
|
|
150
|
+
items: z.array(registryIndexItemSchema)
|
|
151
|
+
});
|
|
152
|
+
var componentNameSchema = z.string().regex(
|
|
153
|
+
/^[a-z][a-z0-9-]*$/,
|
|
154
|
+
"Component name must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens"
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// ../shared/src/themes/primitives.ts
|
|
158
|
+
var defaultPrimitives = {
|
|
159
|
+
typography: {
|
|
160
|
+
xs: 10,
|
|
161
|
+
sm: 12,
|
|
162
|
+
base: 15,
|
|
163
|
+
lg: 18,
|
|
164
|
+
xl: 22,
|
|
165
|
+
"2xl": 28,
|
|
166
|
+
"3xl": 36
|
|
167
|
+
},
|
|
168
|
+
spacing: {
|
|
169
|
+
0: 0,
|
|
170
|
+
0.5: 2,
|
|
171
|
+
1: 4,
|
|
172
|
+
2: 8,
|
|
173
|
+
3: 12,
|
|
174
|
+
4: 16,
|
|
175
|
+
5: 20,
|
|
176
|
+
6: 24,
|
|
177
|
+
8: 32,
|
|
178
|
+
10: 40,
|
|
179
|
+
12: 48,
|
|
180
|
+
16: 64
|
|
181
|
+
},
|
|
182
|
+
fontWeights: {
|
|
183
|
+
regular: 400,
|
|
184
|
+
medium: 500,
|
|
185
|
+
semibold: 600,
|
|
186
|
+
bold: 700
|
|
187
|
+
},
|
|
188
|
+
lineHeights: {
|
|
189
|
+
tight: 1.2,
|
|
190
|
+
normal: 1.4,
|
|
191
|
+
relaxed: 1.6
|
|
192
|
+
},
|
|
193
|
+
borderRadius: {
|
|
194
|
+
none: 0,
|
|
195
|
+
sm: 2,
|
|
196
|
+
md: 4,
|
|
197
|
+
lg: 8,
|
|
198
|
+
full: 9999
|
|
199
|
+
},
|
|
200
|
+
letterSpacing: {
|
|
201
|
+
tight: -0.025,
|
|
202
|
+
normal: 0,
|
|
203
|
+
wide: 0.025,
|
|
204
|
+
wider: 0.05
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// ../shared/src/themes/professional.ts
|
|
209
|
+
var professionalTheme = {
|
|
210
|
+
name: "professional",
|
|
211
|
+
primitives: defaultPrimitives,
|
|
212
|
+
colors: {
|
|
213
|
+
foreground: "#18181b",
|
|
214
|
+
background: "#ffffff",
|
|
215
|
+
muted: "#f4f4f5",
|
|
216
|
+
mutedForeground: "#71717a",
|
|
217
|
+
primary: "#18181b",
|
|
218
|
+
primaryForeground: "#ffffff",
|
|
219
|
+
border: "#e4e4e7",
|
|
220
|
+
accent: "#3b82f6",
|
|
221
|
+
destructive: "#dc2626",
|
|
222
|
+
success: "#16a34a",
|
|
223
|
+
warning: "#d97706",
|
|
224
|
+
info: "#0ea5e9"
|
|
225
|
+
},
|
|
226
|
+
typography: {
|
|
227
|
+
body: {
|
|
228
|
+
fontFamily: "Helvetica",
|
|
229
|
+
fontSize: 11,
|
|
230
|
+
lineHeight: 1.6
|
|
231
|
+
},
|
|
232
|
+
heading: {
|
|
233
|
+
fontFamily: "Times-Roman",
|
|
234
|
+
fontWeight: 700,
|
|
235
|
+
lineHeight: 1.25,
|
|
236
|
+
fontSize: {
|
|
237
|
+
h1: 32,
|
|
238
|
+
h2: 24,
|
|
239
|
+
h3: 20,
|
|
240
|
+
h4: 16,
|
|
241
|
+
h5: 14,
|
|
242
|
+
h6: 12
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
spacing: {
|
|
247
|
+
page: {
|
|
248
|
+
marginTop: 56,
|
|
249
|
+
marginRight: 48,
|
|
250
|
+
marginBottom: 56,
|
|
251
|
+
marginLeft: 48
|
|
252
|
+
},
|
|
253
|
+
sectionGap: 28,
|
|
254
|
+
paragraphGap: 10,
|
|
255
|
+
componentGap: 14
|
|
256
|
+
},
|
|
257
|
+
page: {
|
|
258
|
+
size: "A4",
|
|
259
|
+
orientation: "portrait"
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// ../shared/src/themes/modern.ts
|
|
264
|
+
var modernTheme = {
|
|
265
|
+
name: "modern",
|
|
266
|
+
primitives: defaultPrimitives,
|
|
267
|
+
colors: {
|
|
268
|
+
foreground: "#0f172a",
|
|
269
|
+
background: "#ffffff",
|
|
270
|
+
muted: "#f1f5f9",
|
|
271
|
+
mutedForeground: "#64748b",
|
|
272
|
+
primary: "#334155",
|
|
273
|
+
primaryForeground: "#ffffff",
|
|
274
|
+
border: "#e2e8f0",
|
|
275
|
+
accent: "#6366f1",
|
|
276
|
+
destructive: "#ef4444",
|
|
277
|
+
success: "#22c55e",
|
|
278
|
+
warning: "#f59e0b",
|
|
279
|
+
info: "#3b82f6"
|
|
280
|
+
},
|
|
281
|
+
typography: {
|
|
282
|
+
body: {
|
|
283
|
+
fontFamily: "Helvetica",
|
|
284
|
+
fontSize: 11,
|
|
285
|
+
lineHeight: 1.6
|
|
286
|
+
},
|
|
287
|
+
heading: {
|
|
288
|
+
fontFamily: "Helvetica",
|
|
289
|
+
fontWeight: 600,
|
|
290
|
+
lineHeight: 1.25,
|
|
291
|
+
fontSize: {
|
|
292
|
+
h1: 28,
|
|
293
|
+
h2: 22,
|
|
294
|
+
h3: 18,
|
|
295
|
+
h4: 16,
|
|
296
|
+
h5: 14,
|
|
297
|
+
h6: 12
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
spacing: {
|
|
302
|
+
page: {
|
|
303
|
+
marginTop: 40,
|
|
304
|
+
marginRight: 40,
|
|
305
|
+
marginBottom: 40,
|
|
306
|
+
marginLeft: 40
|
|
307
|
+
},
|
|
308
|
+
sectionGap: 24,
|
|
309
|
+
paragraphGap: 10,
|
|
310
|
+
componentGap: 12
|
|
311
|
+
},
|
|
312
|
+
page: {
|
|
313
|
+
size: "A4",
|
|
314
|
+
orientation: "portrait"
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// ../shared/src/themes/minimal.ts
|
|
319
|
+
var minimalTheme = {
|
|
320
|
+
name: "minimal",
|
|
321
|
+
primitives: defaultPrimitives,
|
|
322
|
+
colors: {
|
|
323
|
+
foreground: "#18181b",
|
|
324
|
+
background: "#ffffff",
|
|
325
|
+
muted: "#fafafa",
|
|
326
|
+
mutedForeground: "#a1a1aa",
|
|
327
|
+
primary: "#18181b",
|
|
328
|
+
primaryForeground: "#ffffff",
|
|
329
|
+
border: "#e4e4e7",
|
|
330
|
+
accent: "#71717a",
|
|
331
|
+
destructive: "#b91c1c",
|
|
332
|
+
success: "#15803d",
|
|
333
|
+
warning: "#a16207",
|
|
334
|
+
info: "#0369a1"
|
|
335
|
+
},
|
|
336
|
+
typography: {
|
|
337
|
+
body: {
|
|
338
|
+
fontFamily: "Helvetica",
|
|
339
|
+
fontSize: 11,
|
|
340
|
+
lineHeight: 1.65
|
|
341
|
+
},
|
|
342
|
+
heading: {
|
|
343
|
+
fontFamily: "Courier",
|
|
344
|
+
fontWeight: 600,
|
|
345
|
+
lineHeight: 1.25,
|
|
346
|
+
fontSize: {
|
|
347
|
+
h1: 24,
|
|
348
|
+
h2: 20,
|
|
349
|
+
h3: 16,
|
|
350
|
+
h4: 14,
|
|
351
|
+
h5: 12,
|
|
352
|
+
h6: 10
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
spacing: {
|
|
357
|
+
page: {
|
|
358
|
+
marginTop: 72,
|
|
359
|
+
marginRight: 56,
|
|
360
|
+
marginBottom: 72,
|
|
361
|
+
marginLeft: 56
|
|
362
|
+
},
|
|
363
|
+
sectionGap: 36,
|
|
364
|
+
paragraphGap: 14,
|
|
365
|
+
componentGap: 18
|
|
366
|
+
},
|
|
367
|
+
page: {
|
|
368
|
+
size: "A4",
|
|
369
|
+
orientation: "portrait"
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// ../shared/src/themes/index.ts
|
|
374
|
+
var themePresets = {
|
|
375
|
+
professional: professionalTheme,
|
|
376
|
+
modern: modernTheme,
|
|
377
|
+
minimal: minimalTheme
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// ../shared/src/errors.ts
|
|
381
|
+
var PdfxError = class extends Error {
|
|
382
|
+
constructor(message, code, suggestion) {
|
|
383
|
+
super(message);
|
|
384
|
+
this.code = code;
|
|
385
|
+
this.suggestion = suggestion;
|
|
386
|
+
this.name = "PdfxError";
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
var RegistryError = class extends PdfxError {
|
|
390
|
+
constructor(message, suggestion) {
|
|
391
|
+
super(message, "REGISTRY_ERROR", suggestion);
|
|
392
|
+
this.name = "RegistryError";
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
var NetworkError = class extends PdfxError {
|
|
396
|
+
constructor(message, suggestion) {
|
|
397
|
+
super(
|
|
398
|
+
message,
|
|
399
|
+
"NETWORK_ERROR",
|
|
400
|
+
suggestion ?? "Check your internet connection and registry URL"
|
|
401
|
+
);
|
|
402
|
+
this.name = "NetworkError";
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// src/constants.ts
|
|
407
|
+
var DEFAULTS = {
|
|
408
|
+
REGISTRY_URL: "https://pdfx.akashpise.dev/r",
|
|
409
|
+
SCHEMA_URL: "https://pdfx.akashpise.dev/schema.json",
|
|
410
|
+
COMPONENT_DIR: "./src/components/pdfx",
|
|
411
|
+
THEME_FILE: "./src/lib/pdfx-theme.ts",
|
|
412
|
+
BLOCK_DIR: "./src/blocks/pdfx"
|
|
413
|
+
};
|
|
414
|
+
var REGISTRY_SUBPATHS = {
|
|
415
|
+
BLOCKS: "blocks"
|
|
416
|
+
};
|
|
417
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
418
|
+
|
|
419
|
+
// src/mcp/utils.ts
|
|
420
|
+
var REGISTRY_BASE = DEFAULTS.REGISTRY_URL;
|
|
421
|
+
var BLOCKS_BASE = `${DEFAULTS.REGISTRY_URL}/${REGISTRY_SUBPATHS.BLOCKS}`;
|
|
422
|
+
async function fetchRegistryIndex() {
|
|
423
|
+
let response;
|
|
424
|
+
try {
|
|
425
|
+
response = await fetch(`${REGISTRY_BASE}/index.json`, {
|
|
426
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
427
|
+
});
|
|
428
|
+
} catch (err) {
|
|
429
|
+
const isTimeout = err instanceof Error && err.name === "TimeoutError";
|
|
430
|
+
throw new NetworkError(
|
|
431
|
+
isTimeout ? "Registry request timed out. Check your internet connection." : "Could not reach the PDFx registry. Check your internet connection."
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (!response.ok) {
|
|
435
|
+
throw new RegistryError(`Registry returned HTTP ${response.status}`);
|
|
436
|
+
}
|
|
437
|
+
let data;
|
|
438
|
+
try {
|
|
439
|
+
data = await response.json();
|
|
440
|
+
} catch {
|
|
441
|
+
throw new RegistryError("Registry returned invalid JSON");
|
|
442
|
+
}
|
|
443
|
+
const result = registrySchema.safeParse(data);
|
|
444
|
+
if (!result.success) {
|
|
445
|
+
throw new RegistryError("Registry index has an unexpected format");
|
|
446
|
+
}
|
|
447
|
+
return result.data.items;
|
|
448
|
+
}
|
|
449
|
+
async function fetchRegistryItem(name, base = REGISTRY_BASE) {
|
|
450
|
+
const url = `${base}/${name}.json`;
|
|
451
|
+
let response;
|
|
452
|
+
try {
|
|
453
|
+
response = await fetch(url, {
|
|
454
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
455
|
+
});
|
|
456
|
+
} catch (err) {
|
|
457
|
+
const isTimeout = err instanceof Error && err.name === "TimeoutError";
|
|
458
|
+
throw new NetworkError(
|
|
459
|
+
isTimeout ? `Registry request timed out for "${name}".` : "Could not reach the PDFx registry. Check your internet connection."
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (!response.ok) {
|
|
463
|
+
if (response.status === 404) {
|
|
464
|
+
throw new RegistryError(
|
|
465
|
+
`"${name}" not found in the registry. Use list_components or list_blocks to see available items.`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
throw new RegistryError(`Registry returned HTTP ${response.status} for "${name}"`);
|
|
469
|
+
}
|
|
470
|
+
let data;
|
|
471
|
+
try {
|
|
472
|
+
data = await response.json();
|
|
473
|
+
} catch {
|
|
474
|
+
throw new RegistryError(`Registry returned invalid JSON for "${name}"`);
|
|
475
|
+
}
|
|
476
|
+
const result = registryItemSchema.safeParse(data);
|
|
477
|
+
if (!result.success) {
|
|
478
|
+
throw new RegistryError(`Unexpected registry format for "${name}"`);
|
|
479
|
+
}
|
|
480
|
+
return result.data;
|
|
481
|
+
}
|
|
482
|
+
function textResponse(text) {
|
|
483
|
+
return { content: [{ type: "text", text }] };
|
|
484
|
+
}
|
|
485
|
+
function errorResponse(error) {
|
|
486
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
487
|
+
return {
|
|
488
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
489
|
+
isError: true
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/mcp/tools/add-command.ts
|
|
494
|
+
var getAddCommandSchema = z2.object({
|
|
495
|
+
items: z2.array(z2.string().min(1)).min(1).describe(
|
|
496
|
+
"Item names to add, e.g. ['table', 'heading'] for components or ['invoice-modern'] for blocks"
|
|
497
|
+
),
|
|
498
|
+
type: z2.enum(["component", "block"]).describe("Whether the items are components or blocks")
|
|
499
|
+
});
|
|
500
|
+
async function getAddCommand(args) {
|
|
501
|
+
const isBlock = args.type === "block";
|
|
502
|
+
const cmd = isBlock ? `npx pdfx-cli block add ${args.items.join(" ")}` : `npx pdfx-cli add ${args.items.join(" ")}`;
|
|
503
|
+
const installDir = isBlock ? "src/blocks/pdfx/" : "src/components/pdfx/";
|
|
504
|
+
const inspectTool = isBlock ? "get_block" : "get_component";
|
|
505
|
+
const itemList = args.items.map((i) => `- \`${i}\``).join("\n");
|
|
506
|
+
return textResponse(dedent`
|
|
507
|
+
# Add Command
|
|
508
|
+
|
|
509
|
+
\`\`\`bash
|
|
510
|
+
${cmd}
|
|
511
|
+
\`\`\`
|
|
512
|
+
|
|
513
|
+
**Items:**
|
|
514
|
+
${itemList}
|
|
515
|
+
|
|
516
|
+
**What this does:**
|
|
517
|
+
- Copies source files into \`${installDir}\`
|
|
518
|
+
- You own the code — no runtime package is added
|
|
519
|
+
${isBlock ? "- The block includes a complete document layout ready to customize" : "- Each component gets its own subdirectory inside componentDir"}
|
|
520
|
+
|
|
521
|
+
**Before running:** make sure \`pdfx.json\` exists. Run \`npx pdfx-cli init\` if not.
|
|
522
|
+
|
|
523
|
+
**See source first:** call \`${inspectTool}\` with the item name to review the code before adding.
|
|
524
|
+
|
|
525
|
+
**After adding:** call \`get_audit_checklist\` to verify your setup is correct.
|
|
526
|
+
`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/mcp/tools/audit.ts
|
|
530
|
+
import dedent2 from "dedent";
|
|
531
|
+
async function getAuditChecklist() {
|
|
532
|
+
return textResponse(dedent2`
|
|
533
|
+
# PDFx Setup Audit Checklist
|
|
534
|
+
|
|
535
|
+
Work through this after adding components or generating PDF document code.
|
|
536
|
+
|
|
537
|
+
## Configuration
|
|
538
|
+
- [ ] \`pdfx.json\` exists in the project root
|
|
539
|
+
- [ ] \`componentDir\` path in \`pdfx.json\` is correct (default: \`./src/components/pdfx\`)
|
|
540
|
+
- [ ] Theme file exists at the path set in \`pdfx.json\` (default: \`./src/lib/pdfx-theme.ts\`)
|
|
541
|
+
|
|
542
|
+
## Dependencies
|
|
543
|
+
- [ ] \`@react-pdf/renderer\` is installed — run \`npm ls @react-pdf/renderer\` to confirm
|
|
544
|
+
- [ ] Version is ≥ 3.0.0 (PDFx requires react-pdf v3+)
|
|
545
|
+
|
|
546
|
+
## Imports
|
|
547
|
+
- [ ] PDFx components use **named exports**: \`import { Table, Heading } from '@/components/pdfx/...'\`
|
|
548
|
+
- [ ] \`Document\` and \`Page\` are imported from \`@react-pdf/renderer\`, not from PDFx
|
|
549
|
+
- [ ] No Tailwind classes, CSS variables, or DOM APIs are used inside PDF components
|
|
550
|
+
- [ ] All styles use \`StyleSheet.create({})\` from \`@react-pdf/renderer\`
|
|
551
|
+
|
|
552
|
+
## Rendering
|
|
553
|
+
- [ ] The root PDF component is **not** inside a React Server Component
|
|
554
|
+
- [ ] Using \`renderToBuffer\`, \`PDFViewer\`, or \`PDFDownloadLink\` to render the document
|
|
555
|
+
- [ ] Root component returns \`<Document><Page>...</Page></Document>\`
|
|
556
|
+
- [ ] No console errors about missing fonts
|
|
557
|
+
|
|
558
|
+
## TypeScript
|
|
559
|
+
- [ ] No TypeScript errors in component files
|
|
560
|
+
- [ ] Theme is typed as \`PdfxTheme\` (imported as a type from \`@pdfx/shared\`)
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## Common Issues & Fixes
|
|
565
|
+
|
|
566
|
+
### "Cannot find module @/components/pdfx/..."
|
|
567
|
+
The component hasn't been added yet. Run:
|
|
568
|
+
\`\`\`bash
|
|
569
|
+
npx pdfx-cli add <component-name>
|
|
570
|
+
\`\`\`
|
|
571
|
+
|
|
572
|
+
### "Invalid hook call"
|
|
573
|
+
PDFx components render to PDF, not to the DOM — React hooks are not supported inside them.
|
|
574
|
+
Move hook calls to the parent component and pass data down as props.
|
|
575
|
+
|
|
576
|
+
### "Text strings must be rendered inside \`<Text>\` component"
|
|
577
|
+
Wrap all string literals in \`<Text>\` from \`@react-pdf/renderer\`:
|
|
578
|
+
\`\`\`tsx
|
|
579
|
+
import { Text } from '@react-pdf/renderer';
|
|
580
|
+
// ✗ Wrong: <View>Hello</View>
|
|
581
|
+
// ✓ Correct: <View><Text>Hello</Text></View>
|
|
582
|
+
\`\`\`
|
|
583
|
+
|
|
584
|
+
### Fonts not loading / rendering as a fallback
|
|
585
|
+
Register custom fonts in your theme file:
|
|
586
|
+
\`\`\`tsx
|
|
587
|
+
import { Font } from '@react-pdf/renderer';
|
|
588
|
+
|
|
589
|
+
Font.register({
|
|
590
|
+
family: 'Inter',
|
|
591
|
+
src: 'https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiA.woff2',
|
|
592
|
+
});
|
|
593
|
+
\`\`\`
|
|
594
|
+
|
|
595
|
+
### PDF renders blank or empty
|
|
596
|
+
Ensure your root component returns a \`<Document>\` with at least one \`<Page>\` inside:
|
|
597
|
+
\`\`\`tsx
|
|
598
|
+
import { Document, Page } from '@react-pdf/renderer';
|
|
599
|
+
|
|
600
|
+
export function MyDocument() {
|
|
601
|
+
return (
|
|
602
|
+
<Document>
|
|
603
|
+
<Page size="A4">
|
|
604
|
+
{/* content here */}
|
|
605
|
+
</Page>
|
|
606
|
+
</Document>
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
\`\`\`
|
|
610
|
+
|
|
611
|
+
### @react-pdf/renderer TypeScript errors
|
|
612
|
+
Install the types package:
|
|
613
|
+
\`\`\`bash
|
|
614
|
+
npm install --save-dev @react-pdf/types
|
|
615
|
+
\`\`\`
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## @react-pdf/renderer Layout Constraints
|
|
620
|
+
|
|
621
|
+
These are **fundamental PDF rendering limitations** — they cannot be fixed with CSS-like
|
|
622
|
+
style tweaks. Understanding them will save hours of debugging.
|
|
623
|
+
|
|
624
|
+
### ⚠️ CRITICAL: Do NOT mix \`<View>\` and \`<Text>\` in the same flex row
|
|
625
|
+
|
|
626
|
+
In HTML, inline elements (spans, badges) can sit next to block text freely.
|
|
627
|
+
In \`@react-pdf/renderer\`, \`View\` and \`Text\` are fundamentally different node types.
|
|
628
|
+
Placing a \`View\`-based component (e.g. \`<Badge>\`, \`<PdfAlert>\`) **inline** alongside
|
|
629
|
+
a \`<Text>\` node in the same flex row causes irrecoverable misalignment, overlap, and
|
|
630
|
+
overflow that no amount of padding, margin, or \`alignItems\` can fix.
|
|
631
|
+
|
|
632
|
+
**Wrong — will cause layout corruption:**
|
|
633
|
+
\`\`\`tsx
|
|
634
|
+
{/* Badge is a View; Text is a Text node — they CANNOT share a flex row */}
|
|
635
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
|
636
|
+
<Text>INV-2026-001</Text>
|
|
637
|
+
<Badge label="PAID" variant="success" />
|
|
638
|
+
</View>
|
|
639
|
+
\`\`\`
|
|
640
|
+
|
|
641
|
+
**Correct — place View-based components on their own line:**
|
|
642
|
+
\`\`\`tsx
|
|
643
|
+
<View style={{ flexDirection: 'column', gap: 2 }}>
|
|
644
|
+
<Text>INV-2026-001</Text>
|
|
645
|
+
<Badge label="PAID" variant="success" />
|
|
646
|
+
</View>
|
|
647
|
+
\`\`\`
|
|
648
|
+
|
|
649
|
+
PDFx components that are \`View\`-based (cannot be mixed inline with \`<Text>\`):
|
|
650
|
+
\`Badge\`, \`PdfAlert\`, \`Card\`, \`Divider\`, \`KeyValue\`, \`Section\`, \`Table\`, \`DataTable\`,
|
|
651
|
+
\`PdfGraph\`, \`PdfImage\`, \`PdfSignatureBlock\`, \`PdfList\`
|
|
652
|
+
|
|
653
|
+
### No \`position: absolute\` stacking inside \`<Text>\` nodes
|
|
654
|
+
Absolute positioning works on \`View\` elements but not inside \`Text\` runs.
|
|
655
|
+
|
|
656
|
+
### \`gap\` only works between \`View\` siblings
|
|
657
|
+
Use \`gap\` on a \`View\` container whose children are all \`View\` elements.
|
|
658
|
+
If any child is a raw \`Text\` node, use \`marginBottom\` on siblings instead.
|
|
659
|
+
|
|
660
|
+
### No percentage-based font sizes
|
|
661
|
+
\`@react-pdf/renderer\` requires numeric pt/px values for \`fontSize\`. Do not use strings like \`"1rem"\` or \`"120%"\`.
|
|
662
|
+
`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/mcp/tools/blocks.ts
|
|
666
|
+
import dedent3 from "dedent";
|
|
667
|
+
import { z as z3 } from "zod";
|
|
668
|
+
var listBlocksSchema = z3.object({});
|
|
669
|
+
async function listBlocks() {
|
|
670
|
+
const items = await fetchRegistryIndex();
|
|
671
|
+
const blocks = items.filter((i) => i.type === "registry:block");
|
|
672
|
+
const invoices = blocks.filter((b) => b.name.startsWith("invoice-"));
|
|
673
|
+
const reports = blocks.filter((b) => b.name.startsWith("report-"));
|
|
674
|
+
const others = blocks.filter(
|
|
675
|
+
(b) => !b.name.startsWith("invoice-") && !b.name.startsWith("report-")
|
|
676
|
+
);
|
|
677
|
+
const formatBlock = (b) => {
|
|
678
|
+
const peers = b.peerComponents?.length ? ` _(requires: ${b.peerComponents.join(", ")})_` : "";
|
|
679
|
+
return `- **${b.name}** \u2014 ${b.description ?? "No description"}${peers}`;
|
|
680
|
+
};
|
|
681
|
+
const sections = [];
|
|
682
|
+
if (invoices.length > 0) {
|
|
683
|
+
sections.push(
|
|
684
|
+
`### Invoice Blocks (${invoices.length})
|
|
685
|
+
${invoices.map(formatBlock).join("\n")}`
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
if (reports.length > 0) {
|
|
689
|
+
sections.push(`### Report Blocks (${reports.length})
|
|
690
|
+
${reports.map(formatBlock).join("\n")}`);
|
|
691
|
+
}
|
|
692
|
+
if (others.length > 0) {
|
|
693
|
+
sections.push(`### Other Blocks (${others.length})
|
|
694
|
+
${others.map(formatBlock).join("\n")}`);
|
|
695
|
+
}
|
|
696
|
+
return textResponse(dedent3`
|
|
697
|
+
# PDFx Blocks (${blocks.length})
|
|
698
|
+
|
|
699
|
+
Blocks are complete, copy-paste ready document layouts. Unlike components, they are full documents ready to customize.
|
|
700
|
+
|
|
701
|
+
${sections.join("\n\n")}
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
Add a block: \`npx pdfx-cli block add <name>\`
|
|
705
|
+
See full source: call \`get_block\` with the block name
|
|
706
|
+
`);
|
|
707
|
+
}
|
|
708
|
+
var getBlockSchema = z3.object({
|
|
709
|
+
block: z3.string().min(1).describe("Block name, e.g. 'invoice-modern', 'report-financial'")
|
|
710
|
+
});
|
|
711
|
+
async function getBlock(args) {
|
|
712
|
+
const item = await fetchRegistryItem(args.block, BLOCKS_BASE);
|
|
713
|
+
const fileList = item.files.map((f) => `- \`${f.path}\``).join("\n");
|
|
714
|
+
const peers = item.peerComponents?.length ? item.peerComponents.join(", ") : "none";
|
|
715
|
+
const fileSources = item.files.map(
|
|
716
|
+
(f) => dedent3`
|
|
717
|
+
### \`${f.path}\`
|
|
718
|
+
\`\`\`tsx
|
|
719
|
+
${f.content}
|
|
720
|
+
\`\`\`
|
|
721
|
+
`
|
|
722
|
+
).join("\n\n");
|
|
723
|
+
return textResponse(dedent3`
|
|
724
|
+
# ${item.title ?? item.name}
|
|
725
|
+
|
|
726
|
+
${item.description ?? ""}
|
|
727
|
+
|
|
728
|
+
## Files
|
|
729
|
+
${fileList}
|
|
730
|
+
|
|
731
|
+
## Required PDFx Components
|
|
732
|
+
${peers}
|
|
733
|
+
|
|
734
|
+
## Add Command
|
|
735
|
+
\`\`\`bash
|
|
736
|
+
npx pdfx-cli block add ${args.block}
|
|
737
|
+
\`\`\`
|
|
738
|
+
|
|
739
|
+
## Source Code
|
|
740
|
+
${fileSources}
|
|
741
|
+
`);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/mcp/tools/components.ts
|
|
745
|
+
import dedent4 from "dedent";
|
|
746
|
+
import { z as z4 } from "zod";
|
|
747
|
+
var listComponentsSchema = z4.object({});
|
|
748
|
+
async function listComponents() {
|
|
749
|
+
const items = await fetchRegistryIndex();
|
|
750
|
+
const components = items.filter((i) => i.type === "registry:ui");
|
|
751
|
+
const rows = components.map((c) => `- **${c.name}** \u2014 ${c.description ?? "No description"}`).join("\n");
|
|
752
|
+
return textResponse(dedent4`
|
|
753
|
+
# PDFx Components (${components.length})
|
|
754
|
+
|
|
755
|
+
${rows}
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
Add a component: \`npx pdfx-cli add <name>\`
|
|
759
|
+
See full source, props, and exact export name: call \`get_component\` with the component name
|
|
760
|
+
`);
|
|
761
|
+
}
|
|
762
|
+
var getComponentSchema = z4.object({
|
|
763
|
+
component: z4.string().min(1).describe("Component name, e.g. 'table', 'heading', 'data-table'")
|
|
764
|
+
});
|
|
765
|
+
async function getComponent(args) {
|
|
766
|
+
const item = await fetchRegistryItem(args.component);
|
|
767
|
+
const fileList = item.files.map((f) => `- \`${f.path}\``).join("\n");
|
|
768
|
+
const deps = item.dependencies?.length ? item.dependencies.join(", ") : "none";
|
|
769
|
+
const devDeps = item.devDependencies?.length ? item.devDependencies.join(", ") : "none";
|
|
770
|
+
const registryDeps = item.registryDependencies?.length ? item.registryDependencies.join(", ") : "none";
|
|
771
|
+
const primaryContent = item.files[0]?.content ?? "";
|
|
772
|
+
const primaryPath = item.files[0]?.path ?? "";
|
|
773
|
+
const exportNames = extractAllExportNames(primaryContent);
|
|
774
|
+
const mainExport = extractExportName(primaryContent);
|
|
775
|
+
const exportSection = exportNames.length > 0 ? dedent4`
|
|
776
|
+
## Exports
|
|
777
|
+
**Main component export:** \`${mainExport ?? exportNames[0]}\`
|
|
778
|
+
|
|
779
|
+
All named exports from \`${primaryPath}\`:
|
|
780
|
+
${exportNames.map((n) => `- \`${n}\``).join("\n")}
|
|
781
|
+
|
|
782
|
+
**Import after \`npx pdfx-cli@latest add ${args.component}\`:**
|
|
783
|
+
\`\`\`tsx
|
|
784
|
+
import { ${mainExport ?? exportNames[0]} } from './components/pdfx/${args.component}/pdfx-${args.component}';
|
|
785
|
+
\`\`\`
|
|
786
|
+
` : "";
|
|
787
|
+
const fileSources = item.files.map(
|
|
788
|
+
(f) => dedent4`
|
|
789
|
+
### \`${f.path}\`
|
|
790
|
+
\`\`\`tsx
|
|
791
|
+
${f.content}
|
|
792
|
+
\`\`\`
|
|
793
|
+
`
|
|
794
|
+
).join("\n\n");
|
|
795
|
+
return textResponse(dedent4`
|
|
796
|
+
# ${item.title ?? item.name}
|
|
797
|
+
|
|
798
|
+
${item.description ?? ""}
|
|
799
|
+
|
|
800
|
+
## Files
|
|
801
|
+
${fileList}
|
|
802
|
+
|
|
803
|
+
## Dependencies
|
|
804
|
+
- Runtime: ${deps}
|
|
805
|
+
- Dev: ${devDeps}
|
|
806
|
+
- Other PDFx components required: ${registryDeps}
|
|
807
|
+
|
|
808
|
+
${exportSection}
|
|
809
|
+
|
|
810
|
+
## Add Command
|
|
811
|
+
\`\`\`bash
|
|
812
|
+
npx pdfx-cli add ${args.component}
|
|
813
|
+
\`\`\`
|
|
814
|
+
|
|
815
|
+
## Source Code
|
|
816
|
+
${fileSources}
|
|
817
|
+
`);
|
|
818
|
+
}
|
|
819
|
+
function extractExportName(source) {
|
|
820
|
+
if (!source) return null;
|
|
821
|
+
const matches = [...source.matchAll(/export\s+(?:function|const)\s+([A-Z][A-Za-z0-9]*)/g)];
|
|
822
|
+
if (matches.length === 0) return null;
|
|
823
|
+
return matches[0][1] ?? null;
|
|
824
|
+
}
|
|
825
|
+
function extractAllExportNames(source) {
|
|
826
|
+
const seen = /* @__PURE__ */ new Set();
|
|
827
|
+
const results = [];
|
|
828
|
+
for (const m of source.matchAll(/export\s+(?:function|const|class)\s+([A-Za-z][A-Za-z0-9]*)/g)) {
|
|
829
|
+
const name = m[1];
|
|
830
|
+
if (name && !seen.has(name)) {
|
|
831
|
+
seen.add(name);
|
|
832
|
+
results.push(name);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
for (const m of source.matchAll(/export\s+\{([^}]+)\}/g)) {
|
|
836
|
+
for (const part of m[1].split(",")) {
|
|
837
|
+
const name = part.trim().split(/\s+as\s+/).pop()?.trim();
|
|
838
|
+
if (name && /^[A-Za-z]/.test(name) && !seen.has(name)) {
|
|
839
|
+
seen.add(name);
|
|
840
|
+
results.push(name);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return results;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/mcp/tools/installation.ts
|
|
848
|
+
import dedent5 from "dedent";
|
|
849
|
+
import { z as z5 } from "zod";
|
|
850
|
+
var getInstallationSchema = z5.object({
|
|
851
|
+
framework: z5.enum(["nextjs", "react", "vite", "remix", "other"]).describe("Target framework"),
|
|
852
|
+
package_manager: z5.enum(["npm", "pnpm", "yarn", "bun"]).describe("Package manager to use")
|
|
853
|
+
});
|
|
854
|
+
function installCmd(pm, pkg, dev = false) {
|
|
855
|
+
const base = { npm: "npm install", pnpm: "pnpm add", yarn: "yarn add", bun: "bun add" }[pm];
|
|
856
|
+
const devFlag = { npm: "--save-dev", pnpm: "-D", yarn: "--dev", bun: "-d" }[pm];
|
|
857
|
+
return dev ? `${base} ${devFlag} ${pkg}` : `${base} ${pkg}`;
|
|
858
|
+
}
|
|
859
|
+
var FRAMEWORK_NOTES = {
|
|
860
|
+
nextjs: dedent5`
|
|
861
|
+
## Next.js Notes
|
|
862
|
+
|
|
863
|
+
**App Router** — Use a Route Handler to serve PDFs:
|
|
864
|
+
\`\`\`tsx
|
|
865
|
+
// app/api/pdf/route.ts
|
|
866
|
+
import { renderToBuffer } from '@react-pdf/renderer';
|
|
867
|
+
import { MyDocument } from '@/components/pdfx/my-document';
|
|
868
|
+
|
|
869
|
+
export async function GET() {
|
|
870
|
+
const buffer = await renderToBuffer(<MyDocument />);
|
|
871
|
+
return new Response(buffer, {
|
|
872
|
+
headers: { 'Content-Type': 'application/pdf' },
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
\`\`\`
|
|
876
|
+
|
|
877
|
+
**Important:** Do NOT render PDFx components inside React Server Components.
|
|
878
|
+
Always use a \`'use client'\` boundary or a Route Handler.
|
|
879
|
+
|
|
880
|
+
**Pages Router** — Use an API route:
|
|
881
|
+
\`\`\`tsx
|
|
882
|
+
// pages/api/pdf.ts
|
|
883
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
884
|
+
import { renderToBuffer } from '@react-pdf/renderer';
|
|
885
|
+
import { MyDocument } from '@/components/pdfx/my-document';
|
|
886
|
+
|
|
887
|
+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
888
|
+
const buffer = await renderToBuffer(<MyDocument />);
|
|
889
|
+
res.setHeader('Content-Type', 'application/pdf');
|
|
890
|
+
res.send(buffer);
|
|
891
|
+
}
|
|
892
|
+
\`\`\`
|
|
893
|
+
`,
|
|
894
|
+
react: dedent5`
|
|
895
|
+
## React Notes
|
|
896
|
+
|
|
897
|
+
Display a PDF inline with \`PDFViewer\`:
|
|
898
|
+
\`\`\`tsx
|
|
899
|
+
import { PDFViewer } from '@react-pdf/renderer';
|
|
900
|
+
import { MyDocument } from './components/pdfx/my-document';
|
|
901
|
+
|
|
902
|
+
export function App() {
|
|
903
|
+
return (
|
|
904
|
+
<PDFViewer width="100%" height="600px">
|
|
905
|
+
<MyDocument />
|
|
906
|
+
</PDFViewer>
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
\`\`\`
|
|
910
|
+
|
|
911
|
+
Trigger a download with \`PDFDownloadLink\`:
|
|
912
|
+
\`\`\`tsx
|
|
913
|
+
import { PDFDownloadLink } from '@react-pdf/renderer';
|
|
914
|
+
import { MyDocument } from './components/pdfx/my-document';
|
|
915
|
+
|
|
916
|
+
export function DownloadButton() {
|
|
917
|
+
return (
|
|
918
|
+
<PDFDownloadLink document={<MyDocument />} fileName="document.pdf">
|
|
919
|
+
{({ loading }) => (loading ? 'Generating PDF...' : 'Download PDF')}
|
|
920
|
+
</PDFDownloadLink>
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
\`\`\`
|
|
924
|
+
`,
|
|
925
|
+
vite: dedent5`
|
|
926
|
+
## Vite Notes
|
|
927
|
+
|
|
928
|
+
Works with both \`vite + react\` and \`vite + react-swc\` templates.
|
|
929
|
+
|
|
930
|
+
For client-side rendering, use \`PDFViewer\` or \`PDFDownloadLink\` from \`@react-pdf/renderer\`.
|
|
931
|
+
|
|
932
|
+
For server-side generation, use a separate Node.js server or Vite's server-side features.
|
|
933
|
+
`,
|
|
934
|
+
remix: dedent5`
|
|
935
|
+
## Remix Notes
|
|
936
|
+
|
|
937
|
+
Use a resource route to serve PDFs:
|
|
938
|
+
\`\`\`tsx
|
|
939
|
+
// app/routes/pdf.tsx
|
|
940
|
+
import { renderToStream } from '@react-pdf/renderer';
|
|
941
|
+
import { MyDocument } from '~/components/pdfx/my-document';
|
|
942
|
+
|
|
943
|
+
export async function loader() {
|
|
944
|
+
const stream = await renderToStream(<MyDocument />);
|
|
945
|
+
return new Response(stream as unknown as ReadableStream, {
|
|
946
|
+
headers: { 'Content-Type': 'application/pdf' },
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
\`\`\`
|
|
950
|
+
`,
|
|
951
|
+
other: dedent5`
|
|
952
|
+
## General Notes
|
|
953
|
+
|
|
954
|
+
\`@react-pdf/renderer\` works in any Node.js ≥ 18 environment.
|
|
955
|
+
|
|
956
|
+
- **Buffer output**: \`await renderToBuffer(<MyDocument />)\`
|
|
957
|
+
- **Stream output**: \`await renderToStream(<MyDocument />)\`
|
|
958
|
+
- **Client-side**: Use \`PDFViewer\` or \`PDFDownloadLink\` from \`@react-pdf/renderer\`
|
|
959
|
+
`
|
|
960
|
+
};
|
|
961
|
+
async function getInstallation(args) {
|
|
962
|
+
const pm = args.package_manager;
|
|
963
|
+
const fw = args.framework;
|
|
964
|
+
return textResponse(dedent5`
|
|
965
|
+
# PDFx Setup Guide: ${fw} + ${pm}
|
|
966
|
+
|
|
967
|
+
## Step 1 — Install the peer dependency
|
|
968
|
+
|
|
969
|
+
\`\`\`bash
|
|
970
|
+
${installCmd(pm, "@react-pdf/renderer")}
|
|
971
|
+
\`\`\`
|
|
972
|
+
|
|
973
|
+
## Step 2 — Initialize PDFx in your project
|
|
974
|
+
|
|
975
|
+
\`\`\`bash
|
|
976
|
+
npx pdfx-cli init
|
|
977
|
+
\`\`\`
|
|
978
|
+
|
|
979
|
+
This creates \`pdfx.json\` in your project root and generates a theme file at \`src/lib/pdfx-theme.ts\`.
|
|
980
|
+
|
|
981
|
+
## Step 3 — Add your first component
|
|
982
|
+
|
|
983
|
+
\`\`\`bash
|
|
984
|
+
npx pdfx-cli add heading text table
|
|
985
|
+
\`\`\`
|
|
986
|
+
|
|
987
|
+
Components are copied into \`src/components/pdfx/\`. You own the source — there is no runtime package dependency.
|
|
988
|
+
|
|
989
|
+
## Step 4 — Or start with a complete document block
|
|
990
|
+
|
|
991
|
+
\`\`\`bash
|
|
992
|
+
npx pdfx-cli block add invoice-modern
|
|
993
|
+
\`\`\`
|
|
994
|
+
|
|
995
|
+
${FRAMEWORK_NOTES[fw]}
|
|
996
|
+
|
|
997
|
+
## Generated pdfx.json
|
|
998
|
+
|
|
999
|
+
\`\`\`json
|
|
1000
|
+
{
|
|
1001
|
+
"$schema": "https://pdfx.akashpise.dev/schema.json",
|
|
1002
|
+
"componentDir": "./src/components/pdfx",
|
|
1003
|
+
"blockDir": "./src/blocks/pdfx",
|
|
1004
|
+
"registry": "https://pdfx.akashpise.dev/r",
|
|
1005
|
+
"theme": "./src/lib/pdfx-theme.ts"
|
|
1006
|
+
}
|
|
1007
|
+
\`\`\`
|
|
1008
|
+
|
|
1009
|
+
## pdfx.json Field Reference
|
|
1010
|
+
|
|
1011
|
+
All four fields are **required**. Relative paths must start with \`./\` or \`../\`.
|
|
1012
|
+
|
|
1013
|
+
| Field | Type | Description | Default |
|
|
1014
|
+
|-------|------|-------------|---------|
|
|
1015
|
+
| \`componentDir\` | string | Where individual components are installed | \`./src/components/pdfx\` |
|
|
1016
|
+
| \`blockDir\` | string | Where full document blocks are installed | \`./src/blocks/pdfx\` |
|
|
1017
|
+
| \`registry\` | string (URL) | Registry base URL (must start with http) | \`https://pdfx.akashpise.dev/r\` |
|
|
1018
|
+
| \`theme\` | string | Path to your generated theme file | \`./src/lib/pdfx-theme.ts\` |
|
|
1019
|
+
|
|
1020
|
+
> **Non-interactive init (CI / AI agents):** pass \`--yes\` to accept all defaults:
|
|
1021
|
+
> \`\`\`bash
|
|
1022
|
+
> npx pdfx-cli init --yes
|
|
1023
|
+
> \`\`\`
|
|
1024
|
+
|
|
1025
|
+
## Troubleshooting
|
|
1026
|
+
|
|
1027
|
+
| Problem | Fix |
|
|
1028
|
+
|---------|-----|
|
|
1029
|
+
| TypeScript errors on \`@react-pdf/renderer\` | \`${installCmd(pm, "@react-pdf/types", true)}\` |
|
|
1030
|
+
| "Cannot find module @/components/pdfx/..." | Run \`npx pdfx-cli@latest add <component>\` to install it |
|
|
1031
|
+
| PDF renders blank | Ensure root returns \`<Document><Page>...</Page></Document>\` |
|
|
1032
|
+
| "Invalid hook call" | PDFx components cannot use React hooks — pass data as props |
|
|
1033
|
+
|
|
1034
|
+
---
|
|
1035
|
+
Next: call \`get_audit_checklist\` to verify your setup is correct.
|
|
1036
|
+
`);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/mcp/tools/search.ts
|
|
1040
|
+
import dedent6 from "dedent";
|
|
1041
|
+
import { z as z6 } from "zod";
|
|
1042
|
+
var searchRegistrySchema = z6.object({
|
|
1043
|
+
query: z6.string().min(1).describe("Search query \u2014 matched against component name, title, and description"),
|
|
1044
|
+
type: z6.enum(["all", "component", "block"]).optional().default("all").describe("Filter by item type (default: all)"),
|
|
1045
|
+
limit: z6.number().int().positive().max(50).optional().default(20).describe("Maximum number of results to return (default: 20, max: 50)")
|
|
1046
|
+
});
|
|
1047
|
+
async function searchRegistry(args) {
|
|
1048
|
+
const items = await fetchRegistryIndex();
|
|
1049
|
+
const q = args.query.toLowerCase();
|
|
1050
|
+
let pool = items;
|
|
1051
|
+
if (args.type === "component") {
|
|
1052
|
+
pool = items.filter((i) => i.type === "registry:ui");
|
|
1053
|
+
} else if (args.type === "block") {
|
|
1054
|
+
pool = items.filter((i) => i.type === "registry:block");
|
|
1055
|
+
}
|
|
1056
|
+
const scored = pool.map((item) => {
|
|
1057
|
+
const name = item.name.toLowerCase();
|
|
1058
|
+
const title = (item.title ?? "").toLowerCase();
|
|
1059
|
+
const desc = (item.description ?? "").toLowerCase();
|
|
1060
|
+
let score = 0;
|
|
1061
|
+
if (name === q) score = 100;
|
|
1062
|
+
else if (name.startsWith(q)) score = 80;
|
|
1063
|
+
else if (name.includes(q)) score = 60;
|
|
1064
|
+
else if (title.includes(q)) score = 40;
|
|
1065
|
+
else if (desc.includes(q)) score = 20;
|
|
1066
|
+
return { item, score };
|
|
1067
|
+
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, args.limit);
|
|
1068
|
+
if (scored.length === 0) {
|
|
1069
|
+
return textResponse(dedent6`
|
|
1070
|
+
# Search: "${args.query}"
|
|
1071
|
+
|
|
1072
|
+
No results found. Try a broader query or browse all items:
|
|
1073
|
+
- \`list_components\` — see all 24 components
|
|
1074
|
+
- \`list_blocks\` — see all 10 blocks
|
|
1075
|
+
`);
|
|
1076
|
+
}
|
|
1077
|
+
const rows = scored.map(({ item }) => {
|
|
1078
|
+
const typeLabel2 = item.type === "registry:ui" ? "component" : "block";
|
|
1079
|
+
const addCmd = item.type === "registry:ui" ? `npx pdfx-cli add ${item.name}` : `npx pdfx-cli block add ${item.name}`;
|
|
1080
|
+
return dedent6`
|
|
1081
|
+
- **${item.name}** _(${typeLabel2})_ — ${item.description ?? "No description"}
|
|
1082
|
+
\`${addCmd}\`
|
|
1083
|
+
`;
|
|
1084
|
+
});
|
|
1085
|
+
const typeLabel = args.type === "all" ? "components + blocks" : args.type === "component" ? "components" : "blocks";
|
|
1086
|
+
return textResponse(dedent6`
|
|
1087
|
+
# Search: "${args.query}" — ${scored.length} result${scored.length === 1 ? "" : "s"} (${typeLabel})
|
|
1088
|
+
|
|
1089
|
+
${rows.join("\n")}
|
|
1090
|
+
`);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/mcp/tools/theme.ts
|
|
1094
|
+
import dedent7 from "dedent";
|
|
1095
|
+
import { z as z7 } from "zod";
|
|
1096
|
+
var getThemeSchema = z7.object({
|
|
1097
|
+
theme: z7.enum(["professional", "modern", "minimal"]).describe("Theme preset name")
|
|
1098
|
+
});
|
|
1099
|
+
async function getTheme(args) {
|
|
1100
|
+
const preset = themePresets[args.theme];
|
|
1101
|
+
const { colors, typography, spacing, page } = preset;
|
|
1102
|
+
return textResponse(dedent7`
|
|
1103
|
+
# PDFx Theme: ${args.theme}
|
|
1104
|
+
|
|
1105
|
+
## Colors
|
|
1106
|
+
| Token | Value |
|
|
1107
|
+
|-------|-------|
|
|
1108
|
+
| foreground | \`${colors.foreground}\` |
|
|
1109
|
+
| background | \`${colors.background}\` |
|
|
1110
|
+
| primary | \`${colors.primary}\` |
|
|
1111
|
+
| primaryForeground | \`${colors.primaryForeground}\` |
|
|
1112
|
+
| accent | \`${colors.accent}\` |
|
|
1113
|
+
| muted | \`${colors.muted}\` |
|
|
1114
|
+
| mutedForeground | \`${colors.mutedForeground}\` |
|
|
1115
|
+
| border | \`${colors.border}\` |
|
|
1116
|
+
| destructive | \`${colors.destructive}\` |
|
|
1117
|
+
| success | \`${colors.success}\` |
|
|
1118
|
+
| warning | \`${colors.warning}\` |
|
|
1119
|
+
| info | \`${colors.info}\` |
|
|
1120
|
+
|
|
1121
|
+
## Typography
|
|
1122
|
+
|
|
1123
|
+
### Body
|
|
1124
|
+
- Font family: \`${typography.body.fontFamily}\`
|
|
1125
|
+
- Font size: ${typography.body.fontSize}pt
|
|
1126
|
+
- Line height: ${typography.body.lineHeight}
|
|
1127
|
+
|
|
1128
|
+
### Headings
|
|
1129
|
+
- Font family: \`${typography.heading.fontFamily}\`
|
|
1130
|
+
- Font weight: ${typography.heading.fontWeight}
|
|
1131
|
+
- Line height: ${typography.heading.lineHeight}
|
|
1132
|
+
- h1: ${typography.heading.fontSize.h1}pt
|
|
1133
|
+
- h2: ${typography.heading.fontSize.h2}pt
|
|
1134
|
+
- h3: ${typography.heading.fontSize.h3}pt
|
|
1135
|
+
- h4: ${typography.heading.fontSize.h4}pt
|
|
1136
|
+
- h5: ${typography.heading.fontSize.h5}pt
|
|
1137
|
+
- h6: ${typography.heading.fontSize.h6}pt
|
|
1138
|
+
|
|
1139
|
+
## Spacing
|
|
1140
|
+
- Page margins: top=${spacing.page.marginTop}pt · right=${spacing.page.marginRight}pt · bottom=${spacing.page.marginBottom}pt · left=${spacing.page.marginLeft}pt
|
|
1141
|
+
- Section gap: ${spacing.sectionGap}pt
|
|
1142
|
+
- Paragraph gap: ${spacing.paragraphGap}pt
|
|
1143
|
+
- Component gap: ${spacing.componentGap}pt
|
|
1144
|
+
|
|
1145
|
+
## Page
|
|
1146
|
+
- Size: ${page.size}
|
|
1147
|
+
- Orientation: ${page.orientation}
|
|
1148
|
+
|
|
1149
|
+
## Apply This Theme
|
|
1150
|
+
\`\`\`bash
|
|
1151
|
+
npx pdfx-cli theme switch ${args.theme}
|
|
1152
|
+
\`\`\`
|
|
1153
|
+
|
|
1154
|
+
## Usage in Components
|
|
1155
|
+
\`\`\`tsx
|
|
1156
|
+
// Access theme values in a PDFx component
|
|
1157
|
+
import type { PdfxTheme } from '@pdfx/shared';
|
|
1158
|
+
|
|
1159
|
+
interface Props {
|
|
1160
|
+
theme: PdfxTheme;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
export function MyComponent({ theme }: Props) {
|
|
1164
|
+
return (
|
|
1165
|
+
<View style={{ backgroundColor: theme.colors.background }}>
|
|
1166
|
+
<Text style={{ color: theme.colors.foreground, fontSize: theme.typography.body.fontSize }}>
|
|
1167
|
+
Content
|
|
1168
|
+
</Text>
|
|
1169
|
+
</View>
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
\`\`\`
|
|
1173
|
+
`);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/mcp/index.ts
|
|
1177
|
+
var server = new Server(
|
|
1178
|
+
{ name: "pdfx", version: "1.0.0" },
|
|
1179
|
+
{ capabilities: { tools: {} } }
|
|
1180
|
+
);
|
|
1181
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1182
|
+
tools: [
|
|
1183
|
+
{
|
|
1184
|
+
name: "list_components",
|
|
1185
|
+
description: "List all available PDFx PDF components with names and descriptions. Call this first to discover what components exist before adding any.",
|
|
1186
|
+
inputSchema: zodToJsonSchema(listComponentsSchema)
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
name: "get_component",
|
|
1190
|
+
description: "Get the full source code, files, and dependencies for a specific PDFx component. Use this to understand the API and props before using it in generated code.",
|
|
1191
|
+
inputSchema: zodToJsonSchema(getComponentSchema)
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
name: "list_blocks",
|
|
1195
|
+
description: "List all PDFx pre-built document blocks (complete invoice and report layouts ready to customize).",
|
|
1196
|
+
inputSchema: zodToJsonSchema(listBlocksSchema)
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
name: "get_block",
|
|
1200
|
+
description: "Get the full source code for a PDFx document block. Returns the complete layout code ready to customize for your use case.",
|
|
1201
|
+
inputSchema: zodToJsonSchema(getBlockSchema)
|
|
1202
|
+
},
|
|
1203
|
+
{
|
|
1204
|
+
name: "search_registry",
|
|
1205
|
+
description: "Search PDFx components and blocks by name or description. Use this when you know what you need but not the exact item name.",
|
|
1206
|
+
inputSchema: zodToJsonSchema(searchRegistrySchema)
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
name: "get_theme",
|
|
1210
|
+
description: "Get the full design token values for a PDFx theme preset (professional, modern, or minimal). Use this to understand colors, typography, and spacing before customizing documents.",
|
|
1211
|
+
inputSchema: zodToJsonSchema(getThemeSchema)
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
name: "get_installation",
|
|
1215
|
+
description: "Get step-by-step PDFx setup instructions for a specific framework and package manager. Use this when setting up PDFx in a new project.",
|
|
1216
|
+
inputSchema: zodToJsonSchema(getInstallationSchema)
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
name: "get_add_command",
|
|
1220
|
+
description: "Get the exact CLI command string to add specific PDFx components or blocks to a project.",
|
|
1221
|
+
inputSchema: zodToJsonSchema(getAddCommandSchema)
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
name: "get_audit_checklist",
|
|
1225
|
+
description: "Get a post-generation checklist to verify PDFx is set up correctly. Call this after adding components or generating PDF document code.",
|
|
1226
|
+
inputSchema: zodToJsonSchema(z8.object({}))
|
|
1227
|
+
}
|
|
1228
|
+
]
|
|
1229
|
+
}));
|
|
1230
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1231
|
+
const args = request.params.arguments ?? {};
|
|
1232
|
+
try {
|
|
1233
|
+
switch (request.params.name) {
|
|
1234
|
+
case "list_components":
|
|
1235
|
+
return await listComponents();
|
|
1236
|
+
case "get_component":
|
|
1237
|
+
return await getComponent(getComponentSchema.parse(args));
|
|
1238
|
+
case "list_blocks":
|
|
1239
|
+
return await listBlocks();
|
|
1240
|
+
case "get_block":
|
|
1241
|
+
return await getBlock(getBlockSchema.parse(args));
|
|
1242
|
+
case "search_registry":
|
|
1243
|
+
return await searchRegistry(searchRegistrySchema.parse(args));
|
|
1244
|
+
case "get_theme":
|
|
1245
|
+
return await getTheme(getThemeSchema.parse(args));
|
|
1246
|
+
case "get_installation":
|
|
1247
|
+
return await getInstallation(getInstallationSchema.parse(args));
|
|
1248
|
+
case "get_add_command":
|
|
1249
|
+
return await getAddCommand(getAddCommandSchema.parse(args));
|
|
1250
|
+
case "get_audit_checklist":
|
|
1251
|
+
return await getAuditChecklist();
|
|
1252
|
+
default:
|
|
1253
|
+
return errorResponse(new Error(`Unknown tool: ${request.params.name}`));
|
|
1254
|
+
}
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
return errorResponse(error);
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
1259
|
+
export {
|
|
1260
|
+
server
|
|
1261
|
+
};
|