@juicedresume/mcp 0.4.0 → 0.4.2
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/dist/index.js +57 -11
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -220,6 +220,7 @@ var MULTI_COLUMN_TEMPLATES = [
|
|
|
220
220
|
"coral",
|
|
221
221
|
"prism",
|
|
222
222
|
"deedy",
|
|
223
|
+
"meridian",
|
|
223
224
|
"nova",
|
|
224
225
|
"axis",
|
|
225
226
|
"kintsugi",
|
|
@@ -238,6 +239,14 @@ var Styling = z.object({
|
|
|
238
239
|
accent: z.string().default("#111111"),
|
|
239
240
|
fontHeading: z.string().default("Playfair Display"),
|
|
240
241
|
fontBody: z.string().default("Inter"),
|
|
242
|
+
// Per-resume font OVERRIDES. Empty string = "use the template's designed
|
|
243
|
+
// font" (the default look). When set, the chosen family wins across every
|
|
244
|
+
// template — including Classic/Sidebar templates that otherwise hard-code
|
|
245
|
+
// their own faces — and is applied identically in the editor preview and the
|
|
246
|
+
// exported PDF. Kept separate from fontHeading/fontBody so existing resumes
|
|
247
|
+
// (which never set these) render exactly as before.
|
|
248
|
+
fontHeadingOverride: z.string().default(""),
|
|
249
|
+
fontBodyOverride: z.string().default(""),
|
|
241
250
|
fontSize: z.number().default(10.5),
|
|
242
251
|
// body pt
|
|
243
252
|
lineHeight: z.number().default(1.5),
|
|
@@ -268,7 +277,7 @@ var Locale = z.object({
|
|
|
268
277
|
dateFormat: z.enum(["MM/YYYY", "MMM YYYY", "YYYY", "DD/MM/YYYY", "MM/DD/YYYY"]).default("MMM YYYY"),
|
|
269
278
|
pageFormat: z.enum(["A4", "Letter"]).default("A4")
|
|
270
279
|
});
|
|
271
|
-
var
|
|
280
|
+
var ResumeObject = z.object({
|
|
272
281
|
id: z.string(),
|
|
273
282
|
name: z.string().default("Untitled"),
|
|
274
283
|
schemaVersion: z.literal(2).default(2),
|
|
@@ -279,6 +288,29 @@ var Resume = z.object({
|
|
|
279
288
|
styling: Styling,
|
|
280
289
|
locale: Locale
|
|
281
290
|
});
|
|
291
|
+
var RICH_TEXT_KEYS = /* @__PURE__ */ new Set(["body", "description"]);
|
|
292
|
+
var URL_KEYS = /* @__PURE__ */ new Set(["photoUrl", "url", "link", "employerLink", "schoolLink", "website", "linkedin", "github", "twitter"]);
|
|
293
|
+
var LONG_KEYS = /* @__PURE__ */ new Set(["skills"]);
|
|
294
|
+
function capFor(key) {
|
|
295
|
+
if (RICH_TEXT_KEYS.has(key)) return 6e4;
|
|
296
|
+
if (LONG_KEYS.has(key)) return 1e4;
|
|
297
|
+
if (URL_KEYS.has(key)) return 2e3;
|
|
298
|
+
return 1e3;
|
|
299
|
+
}
|
|
300
|
+
function capStrings(value, key = "") {
|
|
301
|
+
if (typeof value === "string") {
|
|
302
|
+
const max = capFor(key);
|
|
303
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
304
|
+
}
|
|
305
|
+
if (Array.isArray(value)) return value.map((v) => capStrings(v, key));
|
|
306
|
+
if (value && typeof value === "object") {
|
|
307
|
+
const out = {};
|
|
308
|
+
for (const [k, v] of Object.entries(value)) out[k] = capStrings(v, k);
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
313
|
+
var Resume = z.preprocess((value) => capStrings(value), ResumeObject);
|
|
282
314
|
var CoverLetter = z.object({
|
|
283
315
|
id: z.string(),
|
|
284
316
|
name: z.string().default("Untitled"),
|
|
@@ -3188,6 +3220,10 @@ var CLOUD = CLOUD_TOKEN.length > 0;
|
|
|
3188
3220
|
async function ensure(dir) {
|
|
3189
3221
|
await fs.mkdir(dir, { recursive: true });
|
|
3190
3222
|
}
|
|
3223
|
+
function assertSafeId(id) {
|
|
3224
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(id)) throw new Error(`JuicedResume: invalid resume id "${id}".`);
|
|
3225
|
+
return id;
|
|
3226
|
+
}
|
|
3191
3227
|
async function cloudReq(method, p, body) {
|
|
3192
3228
|
let res;
|
|
3193
3229
|
try {
|
|
@@ -3228,6 +3264,7 @@ async function listResumes() {
|
|
|
3228
3264
|
return out.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
|
|
3229
3265
|
}
|
|
3230
3266
|
async function getResume(id) {
|
|
3267
|
+
assertSafeId(id);
|
|
3231
3268
|
if (CLOUD) {
|
|
3232
3269
|
const data = await cloudReq("GET", `/resumes/${id}`);
|
|
3233
3270
|
return data ? Resume.parse(data) : null;
|
|
@@ -3240,6 +3277,7 @@ async function getResume(id) {
|
|
|
3240
3277
|
}
|
|
3241
3278
|
}
|
|
3242
3279
|
async function saveResume(r) {
|
|
3280
|
+
assertSafeId(r.id);
|
|
3243
3281
|
const next = { ...r, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3244
3282
|
if (CLOUD) {
|
|
3245
3283
|
await cloudReq("PUT", `/resumes/${r.id}`, next);
|
|
@@ -3269,6 +3307,7 @@ async function createBlankResume(name = "Untitled") {
|
|
|
3269
3307
|
return await saveResume(blank);
|
|
3270
3308
|
}
|
|
3271
3309
|
async function deleteResume(id) {
|
|
3310
|
+
assertSafeId(id);
|
|
3272
3311
|
if (CLOUD) {
|
|
3273
3312
|
await cloudReq("DELETE", `/resumes/${id}`);
|
|
3274
3313
|
return;
|
|
@@ -3306,7 +3345,7 @@ function resumeToLatex(r) {
|
|
|
3306
3345
|
{\\LARGE \\textbf{${tex(p.fullName || "Your Name")}}}\\\\[2pt]
|
|
3307
3346
|
${p.title ? `{\\itshape ${tex(p.title)}}\\\\[2pt]` : ""}
|
|
3308
3347
|
${[
|
|
3309
|
-
p.email && `\\href{mailto:${p.email}}{${tex(p.email)}}`,
|
|
3348
|
+
p.email && `\\href{mailto:${texUrl(p.email)}}{${tex(p.email)}}`,
|
|
3310
3349
|
p.phone && tex(p.phone),
|
|
3311
3350
|
p.location && tex(p.location),
|
|
3312
3351
|
p.linkedin && hrefHandle(p.linkedin),
|
|
@@ -3351,7 +3390,7 @@ ${i.description ? htmlToTex(i.description) : ""}
|
|
|
3351
3390
|
return `${header}
|
|
3352
3391
|
${items.map((i) => `
|
|
3353
3392
|
\\noindent\\textbf{${tex(i.name)}}${i.role ? ` --- ${tex(i.role)}` : ""}\\hfill ${tex(dateRange(i.startDate, i.endDate, false, r))}\\\\
|
|
3354
|
-
${i.link ? `\\href{${ensureProto(i.link)}}{${tex(i.link)}}\\\\` : ""}
|
|
3393
|
+
${i.link ? `\\href{${texUrl(ensureProto(i.link))}}{${tex(i.link)}}\\\\` : ""}
|
|
3355
3394
|
${htmlToTex(i.description)}
|
|
3356
3395
|
`).join("\n")}
|
|
3357
3396
|
`;
|
|
@@ -3371,7 +3410,7 @@ ${items.map((i) => `
|
|
|
3371
3410
|
return `${header}
|
|
3372
3411
|
\\begin{itemize}
|
|
3373
3412
|
${items.map(
|
|
3374
|
-
(i) => ` \\item \\textbf{${tex(i.name)}}${i.issuer ? ` --- ${tex(i.issuer)}` : ""}${i.date ? ` (${tex(i.date)})` : ""}${i.link ? ` \\href{${ensureProto(i.link)}}{link}` : ""}`
|
|
3413
|
+
(i) => ` \\item \\textbf{${tex(i.name)}}${i.issuer ? ` --- ${tex(i.issuer)}` : ""}${i.date ? ` (${tex(i.date)})` : ""}${i.link ? ` \\href{${texUrl(ensureProto(i.link))}}{link}` : ""}`
|
|
3375
3414
|
).join("\n")}
|
|
3376
3415
|
\\end{itemize}
|
|
3377
3416
|
`;
|
|
@@ -3405,7 +3444,7 @@ ${i.description ? htmlToTex(i.description) : ""}
|
|
|
3405
3444
|
return `${header}
|
|
3406
3445
|
${items.map((i) => `
|
|
3407
3446
|
\\noindent\\textbf{${tex(i.title)}}${i.publisher ? ` --- ${tex(i.publisher)}` : ""}${i.date ? `\\hfill ${tex(i.date)}` : ""}\\\\
|
|
3408
|
-
${i.link ? `\\href{${ensureProto(i.link)}}{${tex(i.link)}}\\\\` : ""}
|
|
3447
|
+
${i.link ? `\\href{${texUrl(ensureProto(i.link))}}{${tex(i.link)}}\\\\` : ""}
|
|
3409
3448
|
${i.description ? htmlToTex(i.description) : ""}
|
|
3410
3449
|
`).join("\n")}
|
|
3411
3450
|
`;
|
|
@@ -3476,7 +3515,7 @@ function inlineHtmlToTex(html) {
|
|
|
3476
3515
|
const href = m[1];
|
|
3477
3516
|
const end = findCloseTag(src, close + 1, "a");
|
|
3478
3517
|
const inner = src.slice(close + 1, end.start);
|
|
3479
|
-
out += `\\href{${href}}{${inlineHtmlToTex(inner)}}`;
|
|
3518
|
+
out += `\\href{${texUrl(decodeEntities(href))}}{${inlineHtmlToTex(inner)}}`;
|
|
3480
3519
|
i = end.after;
|
|
3481
3520
|
continue;
|
|
3482
3521
|
}
|
|
@@ -3512,10 +3551,13 @@ ${after}`;
|
|
|
3512
3551
|
return html.split(/<\/?p[^>]*>/gi).map((c) => inlineHtmlToTex(c).trim()).filter(Boolean).join("\\\\\n");
|
|
3513
3552
|
}
|
|
3514
3553
|
function sanitizeColor(hex) {
|
|
3515
|
-
return hex.replace(
|
|
3554
|
+
return hex.replace(/[^0-9a-fA-F]/g, "").slice(0, 6).padEnd(6, "0");
|
|
3555
|
+
}
|
|
3556
|
+
function texUrl(s) {
|
|
3557
|
+
return s.replace(/[{}\\\s"]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0"));
|
|
3516
3558
|
}
|
|
3517
3559
|
function hrefHandle(s) {
|
|
3518
|
-
return `\\href{${ensureProto(s)}}{${tex(s)}}`;
|
|
3560
|
+
return `\\href{${texUrl(ensureProto(s))}}{${tex(s)}}`;
|
|
3519
3561
|
}
|
|
3520
3562
|
function ensureProto(s) {
|
|
3521
3563
|
if (/^https?:\/\//i.test(s)) return s;
|
|
@@ -3616,8 +3658,12 @@ var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ?
|
|
|
3616
3658
|
async function mutate(id, fn) {
|
|
3617
3659
|
const r = await getResume(id);
|
|
3618
3660
|
if (!r) throw new Error(`Resume not found: ${id}`);
|
|
3619
|
-
const
|
|
3620
|
-
|
|
3661
|
+
const parsed = Resume.safeParse(fn(r));
|
|
3662
|
+
if (!parsed.success) {
|
|
3663
|
+
const first = parsed.error.issues[0];
|
|
3664
|
+
throw new Error(`Invalid patch \u2014 resume would no longer validate (${first?.path.join(".")}: ${first?.message}).`);
|
|
3665
|
+
}
|
|
3666
|
+
return await saveResume(parsed.data);
|
|
3621
3667
|
}
|
|
3622
3668
|
var TOOLS = [
|
|
3623
3669
|
// discovery
|
|
@@ -3696,7 +3742,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
3696
3742
|
case "add_section":
|
|
3697
3743
|
return ok(await mutate(args.id, (r) => {
|
|
3698
3744
|
const type = args.type;
|
|
3699
|
-
if (!DEFAULT_SECTION_TITLES
|
|
3745
|
+
if (!Object.hasOwn(DEFAULT_SECTION_TITLES, type)) throw new Error(`unknown section type: ${type}`);
|
|
3700
3746
|
const section = { id: newId(), type, title: args.title || DEFAULT_SECTION_TITLES[type], visible: true, columns: 1, items: [] };
|
|
3701
3747
|
return { ...r, sections: [...r.sections, section] };
|
|
3702
3748
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juicedresume/mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"mcpName": "com.juicedresume/mcp",
|
|
4
5
|
"description": "Model Context Protocol server for JuicedResume — score, tailor, and edit your resume from any MCP client (Claude Code, Claude Desktop, Cursor).",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"mcp",
|
|
@@ -14,8 +15,7 @@
|
|
|
14
15
|
"homepage": "https://juicedresume.com",
|
|
15
16
|
"repository": {
|
|
16
17
|
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/HarshitSingh-PM/juicedresume.git"
|
|
18
|
-
"directory": "apps/mcp"
|
|
18
|
+
"url": "git+https://github.com/HarshitSingh-PM/juicedresume-mcp.git"
|
|
19
19
|
},
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"author": "Harshit Singh <harshit@halacarly.com>",
|
|
@@ -55,4 +55,4 @@
|
|
|
55
55
|
"publishConfig": {
|
|
56
56
|
"access": "public"
|
|
57
57
|
}
|
|
58
|
-
}
|
|
58
|
+
}
|