resuml 1.8.2 → 1.9.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.
@@ -1258,11 +1258,373 @@ function restoreStdout() {
1258
1258
  console.log = originalLog;
1259
1259
  console.warn = originalWarn;
1260
1260
  }
1261
+ var JSON_RESUME_SCHEMA_REFERENCE = `# JSON Resume Schema Reference
1262
+
1263
+ The JSON Resume schema defines the structure for resume data. resuml uses YAML as the input format.
1264
+
1265
+ ## Top-level sections
1266
+
1267
+ | Section | Required | Description |
1268
+ |---------|----------|-------------|
1269
+ | basics | Yes | Name, label, email, phone, url, summary, location, profiles |
1270
+ | work | Recommended | Work experience entries |
1271
+ | education | Recommended | Education entries |
1272
+ | skills | Recommended | Skill categories with keywords |
1273
+ | projects | Optional | Project entries |
1274
+ | volunteer | Optional | Volunteer experience |
1275
+ | awards | Optional | Awards and honors |
1276
+ | certificates | Optional | Professional certifications |
1277
+ | publications | Optional | Published works |
1278
+ | languages | Optional | Language proficiencies |
1279
+ | interests | Optional | Personal interests |
1280
+ | references | Optional | Professional references |
1281
+
1282
+ ## Section schemas
1283
+
1284
+ ### basics
1285
+ \`\`\`yaml
1286
+ basics:
1287
+ name: "Full Name" # required
1288
+ label: "Professional Title"
1289
+ email: "email@example.com"
1290
+ phone: "+1-555-123-4567"
1291
+ url: "https://website.com"
1292
+ summary: "2-4 sentence professional summary"
1293
+ location:
1294
+ city: "City"
1295
+ countryCode: "US"
1296
+ region: "State"
1297
+ profiles:
1298
+ - network: "LinkedIn"
1299
+ username: "username"
1300
+ url: "https://linkedin.com/in/username"
1301
+ \`\`\`
1302
+
1303
+ ### work
1304
+ \`\`\`yaml
1305
+ work:
1306
+ - name: "Company Name"
1307
+ position: "Job Title"
1308
+ url: "https://company.com"
1309
+ startDate: "2020-01-01" # ISO 8601
1310
+ endDate: "2023-12-31" # omit for current position
1311
+ summary: "Role description"
1312
+ highlights:
1313
+ - "Achievement with measurable result"
1314
+ \`\`\`
1315
+
1316
+ ### education
1317
+ \`\`\`yaml
1318
+ education:
1319
+ - institution: "University"
1320
+ area: "Field of Study"
1321
+ studyType: "Degree Type" # e.g. Bachelor, Master, PhD
1322
+ startDate: "2014-09-01"
1323
+ endDate: "2018-06-01"
1324
+ \`\`\`
1325
+
1326
+ ### skills
1327
+ \`\`\`yaml
1328
+ skills:
1329
+ - name: "Category"
1330
+ level: "Expert" # Master, Expert, Advanced, Intermediate, Beginner
1331
+ keywords: ["Skill1", "Skill2"]
1332
+ \`\`\`
1333
+
1334
+ ### projects
1335
+ \`\`\`yaml
1336
+ projects:
1337
+ - name: "Project Name"
1338
+ description: "What it does"
1339
+ highlights: ["Key achievement"]
1340
+ keywords: ["Tech1", "Tech2"]
1341
+ startDate: "2023-01-01"
1342
+ url: "https://github.com/..."
1343
+ \`\`\`
1344
+
1345
+ ### certificates
1346
+ \`\`\`yaml
1347
+ certificates:
1348
+ - name: "Certificate Name"
1349
+ date: "2023-01-01"
1350
+ issuer: "Issuing Organization"
1351
+ url: "https://credential-url.com"
1352
+ \`\`\`
1353
+
1354
+ ### languages
1355
+ \`\`\`yaml
1356
+ languages:
1357
+ - language: "English"
1358
+ fluency: "Native speaker" # Native speaker, Fluent, Advanced, Intermediate, Elementary
1359
+ \`\`\`
1360
+
1361
+ ## Formatting rules
1362
+ - Dates: ISO 8601 format (YYYY-MM-DD or YYYY-MM)
1363
+ - Start highlights with action verbs: Developed, Implemented, Led, Optimized, Reduced, Built, Designed
1364
+ - Include numbers in 50%+ of highlights (e.g., "Reduced latency by 40%")
1365
+ - Never use first person (I, my, me, we)
1366
+ - Summary: 2-4 sentences positioning the candidate for the specific role
1367
+ `;
1368
+ var ATS_SCORING_RUBRIC = `# ATS Scoring Rubric
1369
+
1370
+ resuml performs deterministic, offline ATS (Applicant Tracking System) analysis.
1371
+
1372
+ ## Scoring system
1373
+
1374
+ ### Rating scale
1375
+ | Score | Rating | Description |
1376
+ |-------|--------|-------------|
1377
+ | 90-100 | Excellent | Resume is well-optimized for ATS |
1378
+ | 75-89 | Good | Resume passes most ATS checks |
1379
+ | 60-74 | Needs Work | Several improvements recommended |
1380
+ | 0-59 | Poor | Significant issues found |
1381
+
1382
+ ### Weight system
1383
+ Each check has a weight that affects the final score:
1384
+ - **High weight (3x)**: Critical checks that significantly impact ATS parsing
1385
+ - **Medium weight (2x)**: Important but not critical checks
1386
+ - **Low weight (1x)**: Nice-to-have improvements
1387
+
1388
+ ### Combined scoring (with job description)
1389
+ When a job description is provided:
1390
+ - Generic checks: 60% of final score
1391
+ - Keyword match: 40% of final score
1392
+
1393
+ ## Checks performed
1394
+
1395
+ ### Contact Information (category: contact)
1396
+ | Check | Weight | What it verifies |
1397
+ |-------|--------|-----------------|
1398
+ | contact-complete | High | Name, email, phone, and city are all present |
1399
+ | has-linkedin | Medium | LinkedIn profile exists in profiles section |
1400
+
1401
+ ### Content Quality (category: content)
1402
+ | Check | Weight | What it verifies |
1403
+ |-------|--------|-----------------|
1404
+ | has-summary | High | Professional summary exists (15-100 words) |
1405
+ | work-highlights | High | Each work entry has at least 2 highlights |
1406
+ | action-verbs | Medium | Highlights start with action verbs |
1407
+ | quantified-impact | Medium | 50%+ of highlights include numbers/metrics |
1408
+ | no-first-person | Low | No first-person pronouns (I, my, me, we) |
1409
+
1410
+ ### Resume Structure (category: structure)
1411
+ | Check | Weight | What it verifies |
1412
+ |-------|--------|-----------------|
1413
+ | date-consistency | Medium | All dates are valid ISO 8601 format |
1414
+ | skills-populated | Medium | At least 3 skill categories defined |
1415
+ | education-complete | Medium | Education section has institution and area |
1416
+ | essential-sections | High | Work, education, and skills sections present |
1417
+
1418
+ ## Job description matching
1419
+ When a job description is provided, resuml extracts keywords using TF-based extraction
1420
+ and matches them against the resume using stem matching. Results include:
1421
+ - **matched**: Keywords found in the resume
1422
+ - **missing**: Keywords not found (add these to improve score)
1423
+ - **matchPercentage**: Percentage of JD keywords found in resume
1424
+
1425
+ ## Tips for improving ATS score
1426
+ 1. Include all contact information (name, email, phone, city)
1427
+ 2. Add a LinkedIn profile URL
1428
+ 3. Write a 2-4 sentence professional summary
1429
+ 4. Use action verbs to start each highlight
1430
+ 5. Quantify achievements with numbers (%, $, time saved, team size)
1431
+ 6. Include at least 3 skill categories with relevant keywords
1432
+ 7. When targeting a job, mirror exact terminology from the job description
1433
+ `;
1261
1434
  function createServer() {
1262
1435
  const server = new import_mcp.McpServer({
1263
1436
  name: "resuml",
1264
1437
  version: "1.0.0"
1265
1438
  });
1439
+ server.registerResource(
1440
+ "json-resume-schema",
1441
+ "resuml://schema/json-resume",
1442
+ {
1443
+ description: "JSON Resume schema reference with all sections, field types, and formatting rules",
1444
+ mimeType: "text/markdown"
1445
+ },
1446
+ () => ({
1447
+ contents: [{
1448
+ uri: "resuml://schema/json-resume",
1449
+ mimeType: "text/markdown",
1450
+ text: JSON_RESUME_SCHEMA_REFERENCE
1451
+ }]
1452
+ })
1453
+ );
1454
+ server.registerResource(
1455
+ "ats-scoring-rubric",
1456
+ "resuml://docs/ats-scoring",
1457
+ {
1458
+ description: "ATS scoring rubric: checks performed, weight system, rating scale, and tips for improving score",
1459
+ mimeType: "text/markdown"
1460
+ },
1461
+ () => ({
1462
+ contents: [{
1463
+ uri: "resuml://docs/ats-scoring",
1464
+ mimeType: "text/markdown",
1465
+ text: ATS_SCORING_RUBRIC
1466
+ }]
1467
+ })
1468
+ );
1469
+ server.registerResource(
1470
+ "theme-catalog",
1471
+ "resuml://themes/catalog",
1472
+ {
1473
+ description: "Available resume themes with descriptions and installation status",
1474
+ mimeType: "application/json"
1475
+ },
1476
+ () => {
1477
+ const themes = KNOWN_THEMES.map((t) => ({
1478
+ name: t.name,
1479
+ package: t.pkg,
1480
+ description: t.description,
1481
+ installed: isThemeInstalled(t.pkg),
1482
+ version: getInstalledVersion(t.pkg)
1483
+ }));
1484
+ return {
1485
+ contents: [{
1486
+ uri: "resuml://themes/catalog",
1487
+ mimeType: "application/json",
1488
+ text: JSON.stringify({ themes, totalCount: themes.length }, null, 2)
1489
+ }]
1490
+ };
1491
+ }
1492
+ );
1493
+ server.registerPrompt(
1494
+ "tailor-resume-to-jd",
1495
+ {
1496
+ title: "Tailor Resume to Job Description",
1497
+ description: "Generate a tailored resume YAML optimized for a specific job description",
1498
+ argsSchema: {
1499
+ jobDescription: import_zod.z.string().describe("The full job description text"),
1500
+ candidateName: import_zod.z.string().optional().describe("Candidate full name"),
1501
+ candidateEmail: import_zod.z.string().optional().describe("Candidate email address"),
1502
+ candidateBackground: import_zod.z.string().optional().describe("Brief summary of the candidate background, skills, and experience to incorporate")
1503
+ }
1504
+ },
1505
+ ({ jobDescription, candidateName, candidateEmail, candidateBackground }) => ({
1506
+ messages: [{
1507
+ role: "user",
1508
+ content: {
1509
+ type: "text",
1510
+ text: `Create a tailored resume in YAML format optimized for the following job description.
1511
+
1512
+ ## Job Description
1513
+ ${jobDescription}
1514
+
1515
+ ${candidateBackground ? `## Candidate Background
1516
+ ${candidateBackground}
1517
+ ` : ""}
1518
+ ## Instructions
1519
+
1520
+ 1. Analyze the job description to identify required skills, technologies, experience level, and industry terms
1521
+ 2. Generate a complete resume YAML following the JSON Resume schema (read the resuml://schema/json-resume resource for the full schema)
1522
+ 3. Mirror exact terminology from the job description in skills and highlights
1523
+ 4. Start every highlight with an action verb (Developed, Implemented, Led, Optimized, Reduced, Built, Designed)
1524
+ 5. Include numbers in 50%+ of highlights (e.g., "Reduced latency by 40%", "Managed team of 8")
1525
+ 6. Never use "I", "my", "me", "we"
1526
+ 7. Write a 2-4 sentence summary positioning the candidate for this specific role
1527
+ 8. Use ISO 8601 dates (YYYY-MM-DD or YYYY-MM)
1528
+ ${candidateName ? `9. Use candidate name: ${candidateName}` : ""}
1529
+ ${candidateEmail ? `10. Use candidate email: ${candidateEmail}` : ""}
1530
+
1531
+ ## Workflow
1532
+ After generating the YAML:
1533
+ 1. Use \`resuml_validate\` to check schema compliance
1534
+ 2. Use \`resuml_ats_check\` with the job description text. Target: score >= 75, keyword match >= 70%
1535
+ 3. If ATS score is low, revise the YAML and re-check
1536
+ 4. Use \`resuml_render\` with theme "even" for the final output
1537
+
1538
+ Output the resume YAML first, then run the validation and ATS check tools.`
1539
+ }
1540
+ }]
1541
+ })
1542
+ );
1543
+ server.registerPrompt(
1544
+ "optimize-ats-score",
1545
+ {
1546
+ title: "Optimize ATS Score",
1547
+ description: "Analyze and improve an existing resume YAML to maximize its ATS score",
1548
+ argsSchema: {
1549
+ resumeYaml: import_zod.z.string().describe("The current resume YAML content"),
1550
+ jobDescription: import_zod.z.string().optional().describe("Optional job description to optimize against"),
1551
+ targetScore: import_zod.z.string().optional().describe("Target ATS score (default: 85)")
1552
+ }
1553
+ },
1554
+ ({ resumeYaml, jobDescription, targetScore }) => ({
1555
+ messages: [{
1556
+ role: "user",
1557
+ content: {
1558
+ type: "text",
1559
+ text: `Optimize this resume YAML to maximize its ATS score${targetScore ? ` (target: ${targetScore})` : " (target: 85+)"}.
1560
+
1561
+ ## Current Resume YAML
1562
+ \`\`\`yaml
1563
+ ${resumeYaml}
1564
+ \`\`\`
1565
+
1566
+ ${jobDescription ? `## Job Description
1567
+ ${jobDescription}
1568
+ ` : ""}
1569
+ ## Instructions
1570
+
1571
+ 1. First, run \`resuml_ats_check\` on the current YAML${jobDescription ? " with the job description" : ""} to get the baseline score
1572
+ 2. Read the ATS scoring rubric (resuml://docs/ats-scoring) to understand what checks are performed
1573
+ 3. Review each failed or low-scoring check and fix the issues:
1574
+ - Missing contact info \u2192 add it
1575
+ - No summary \u2192 write a 2-4 sentence professional summary
1576
+ - Weak highlights \u2192 rewrite with action verbs and quantified metrics
1577
+ - Missing keywords \u2192 incorporate them naturally into skills and highlights
1578
+ - Structural issues \u2192 ensure all essential sections are present
1579
+ 4. Run \`resuml_ats_check\` again to verify improvement
1580
+ 5. Repeat until the target score is reached
1581
+
1582
+ Output the improved YAML with a summary of changes made.`
1583
+ }
1584
+ }]
1585
+ })
1586
+ );
1587
+ server.registerPrompt(
1588
+ "review-resume",
1589
+ {
1590
+ title: "Review Resume",
1591
+ description: "Comprehensive review of a resume YAML with ATS analysis and improvement suggestions",
1592
+ argsSchema: {
1593
+ resumeYaml: import_zod.z.string().describe("The resume YAML content to review")
1594
+ }
1595
+ },
1596
+ ({ resumeYaml }) => ({
1597
+ messages: [{
1598
+ role: "user",
1599
+ content: {
1600
+ type: "text",
1601
+ text: `Perform a comprehensive review of this resume.
1602
+
1603
+ ## Resume YAML
1604
+ \`\`\`yaml
1605
+ ${resumeYaml}
1606
+ \`\`\`
1607
+
1608
+ ## Review steps
1609
+
1610
+ 1. Run \`resuml_validate\` to check schema compliance
1611
+ 2. Run \`resuml_ats_check\` for ATS analysis
1612
+ 3. Review content quality:
1613
+ - Is the summary compelling and role-specific?
1614
+ - Do highlights use strong action verbs?
1615
+ - Are achievements quantified with metrics?
1616
+ - Are skills well-organized and comprehensive?
1617
+ - Is the work history clear and impactful?
1618
+ 4. Provide a structured review with:
1619
+ - **ATS Score**: Current score and rating
1620
+ - **Schema Issues**: Any validation errors
1621
+ - **Strengths**: What the resume does well
1622
+ - **Improvements**: Specific, actionable suggestions
1623
+ - **Revised YAML**: An improved version if significant changes are recommended`
1624
+ }
1625
+ }]
1626
+ })
1627
+ );
1266
1628
  server.registerTool(
1267
1629
  "resuml_init_resume",
1268
1630
  {
@@ -1348,15 +1710,20 @@ function createServer() {
1348
1710
  description: "Render a resume to HTML using a specified theme",
1349
1711
  inputSchema: {
1350
1712
  yaml: import_zod.z.string().describe("Resume content in YAML format"),
1351
- theme: import_zod.z.string().default("even").describe("Theme name (e.g. even, stackoverflow, elegant, paper, kendall)")
1713
+ theme: import_zod.z.string().default("even").describe("Theme name (e.g. even, stackoverflow, elegant, paper, kendall)"),
1714
+ locale: import_zod.z.string().optional().describe("Locale for theme rendering (e.g. en, de)")
1352
1715
  }
1353
1716
  },
1354
- async ({ yaml, theme }) => {
1717
+ async (args) => {
1718
+ const { yaml, theme } = args;
1719
+ const locale = args["locale"];
1355
1720
  suppressStdout();
1356
1721
  try {
1357
1722
  const resume = await processResumeData([yaml]);
1358
1723
  const themeModule = loadTheme(theme, { autoInstall: false });
1359
- const html = await themeModule.render(resume);
1724
+ const renderOptions = {};
1725
+ if (locale) renderOptions["locale"] = locale;
1726
+ const html = await themeModule.render(resume, renderOptions);
1360
1727
  restoreStdout();
1361
1728
  return { content: [{ type: "text", text: html }] };
1362
1729
  } catch (e) {
@@ -1397,15 +1764,22 @@ function createServer() {
1397
1764
  inputSchema: {
1398
1765
  yaml: import_zod.z.string().describe("Resume content in YAML format"),
1399
1766
  theme: import_zod.z.string().default("even").describe("Theme name"),
1400
- format: import_zod.z.enum(["A4", "Letter"]).default("A4").describe("Paper format")
1767
+ format: import_zod.z.enum(["A4", "Letter"]).default("A4").describe("Paper format"),
1768
+ locale: import_zod.z.string().optional().describe("Locale for theme rendering (e.g. en, de)"),
1769
+ margin: import_zod.z.string().optional().describe('Page margins. Single value (e.g. "10mm") for all sides, two values (e.g. "10mm,15mm") for vertical/horizontal, or four values (e.g. "10mm,15mm,10mm,15mm") for top/right/bottom/left')
1401
1770
  }
1402
1771
  },
1403
- async ({ yaml, theme, format }) => {
1772
+ async (args) => {
1773
+ const { yaml, theme, format } = args;
1774
+ const locale = args["locale"];
1775
+ const margin = args["margin"];
1404
1776
  suppressStdout();
1405
1777
  try {
1406
1778
  const resume = await processResumeData([yaml]);
1407
1779
  const themeModule = loadTheme(theme, { autoInstall: false });
1408
- const html = await themeModule.render(resume);
1780
+ const renderOptions = {};
1781
+ if (locale) renderOptions["locale"] = locale;
1782
+ const html = await themeModule.render(resume, renderOptions);
1409
1783
  let chromium;
1410
1784
  try {
1411
1785
  const pw = await import("playwright");
@@ -1417,14 +1791,14 @@ function createServer() {
1417
1791
  isError: true
1418
1792
  };
1419
1793
  }
1794
+ const parsedMargin = parseMargin(margin);
1420
1795
  const browser = await chromium.launch({ headless: true });
1421
1796
  const page = await browser.newPage();
1422
1797
  await page.setContent(html, { waitUntil: "networkidle" });
1423
- const margin = "10mm";
1424
1798
  const pdfBuffer = await page.pdf({
1425
1799
  format,
1426
1800
  printBackground: true,
1427
- margin: { top: margin, bottom: margin, left: margin, right: margin }
1801
+ margin: parsedMargin
1428
1802
  });
1429
1803
  await browser.close();
1430
1804
  restoreStdout();
@@ -1449,6 +1823,21 @@ function createServer() {
1449
1823
  );
1450
1824
  return server;
1451
1825
  }
1826
+ function parseMargin(margin) {
1827
+ const defaultMargin = { top: "10mm", right: "10mm", bottom: "10mm", left: "10mm" };
1828
+ if (!margin) return defaultMargin;
1829
+ const parts = margin.split(",").map((s) => s.trim());
1830
+ if (parts.length === 1 && parts[0]) {
1831
+ return { top: parts[0], right: parts[0], bottom: parts[0], left: parts[0] };
1832
+ }
1833
+ if (parts.length === 2 && parts[0] && parts[1]) {
1834
+ return { top: parts[0], right: parts[1], bottom: parts[0], left: parts[1] };
1835
+ }
1836
+ if (parts.length === 4 && parts[0] && parts[1] && parts[2] && parts[3]) {
1837
+ return { top: parts[0], right: parts[1], bottom: parts[2], left: parts[3] };
1838
+ }
1839
+ return defaultMargin;
1840
+ }
1452
1841
  async function startMcpServer() {
1453
1842
  const server = createServer();
1454
1843
  const transport = new import_stdio.StdioServerTransport();