Skip to content

Commit 900eea1

Browse files
committed
Sprint 2.4-2.6 + QA: skill, registry metadata, MCP docs, CI handshake gate
- skills/cvx/SKILL.md (agentskills.io format): the full CVX loop for skill-capable agents — MCP tools when connected, CLI otherwise, schema summary, truthfulness rules; ships in the tarball; docsSync tripwire now covers it (every content file, all four tools, frontmatter limits) - registry prep: mcpName io.github.hrtips/cvx in package.json, mcp/ model-context-protocol keywords, server.json for mcp-publisher (stable 1.4.0 — submit after the stable release), GitHub topics set - docs: README "Plug it into your agent (MCP)" section; ai-guide Route E (MCP) with per-client one-liners - CI packaged E2E: MCP smoke — spawns the packed tarball's server via node (Windows-safe), completes the initialize handshake, asserts the four-tool inventory - 112 tests; tarball 326 kB (guardrail <500 kB)
1 parent 3b1cfbe commit 900eea1

7 files changed

Lines changed: 158 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,35 @@ jobs:
6060
set -e
6161
test "$CODE" -eq 64
6262
63+
# MCP: initialize handshake + tools/list against the packed tarball
64+
cat > mcp-smoke.cjs <<'SMOKE'
65+
const { spawn } = require("child_process");
66+
const p = spawn(process.execPath, ["node_modules/@hrtips/cvx/bin/cvx.js", "mcp"], { stdio: ["pipe", "pipe", "inherit"] });
67+
let buf = ""; const msgs = [];
68+
const send = (o) => p.stdin.write(JSON.stringify(o) + "\n");
69+
p.stdout.on("data", (d) => {
70+
buf += d;
71+
let i; while ((i = buf.indexOf("\n")) !== -1) {
72+
const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1);
73+
if (line) { msgs.push(JSON.parse(line)); step(); }
74+
}
75+
});
76+
function step() {
77+
if (msgs.length === 1) {
78+
if (msgs[0].result.serverInfo.name !== "cvx") { console.error("bad serverInfo"); process.exit(1); }
79+
send({ jsonrpc: "2.0", method: "notifications/initialized" });
80+
send({ jsonrpc: "2.0", id: 2, method: "tools/list" });
81+
} else if (msgs.length === 2) {
82+
const tools = msgs[1].result.tools.map((t) => t.name).sort().join(",");
83+
if (tools !== "build_pdf,get_schema,init_cv,validate_cv") { console.error("bad tools:", tools); process.exit(1); }
84+
console.log("mcp smoke ok:", tools); p.kill(); process.exit(0);
85+
}
86+
}
87+
send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "ci", version: "0" } } });
88+
setTimeout(() => { console.error("mcp handshake timeout"); process.exit(1); }, 30000);
89+
SMOKE
90+
node mcp-smoke.cjs
91+
6392
npx cvx build
6493
npx cvx build --ats
6594
test -f bruce-wayne.pdf && test -f bruce-wayne-ats.pdf

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,19 @@ The formats are deliberately LLM-friendly, and the schema is published for machi
171171

172172
Save the generated files into `cv-content/`, drop in your photo, run `npx @hrtips/cvx build`. No web access in your assistant? Use the [self-contained prompt](docs/ai-guide.md#route-c--chat-assistant-self-contained-prompt).
173173

174+
### Plug it into your agent (MCP)
175+
176+
CVX ships an MCP server — any MCP client (Claude Desktop, Claude Code, Cursor, VS Code, …) can drive the whole loop with four tools: `get_schema`, `init_cv`, `validate_cv`, `build_pdf`. No API keys, fully offline.
177+
178+
```bash
179+
npx @hrtips/cvx mcp init --client claude # Claude Code (.mcp.json, project)
180+
npx @hrtips/cvx mcp init --client claude-desktop # Claude Desktop (global config)
181+
npx @hrtips/cvx mcp init --client cursor # Cursor (.cursor/mcp.json)
182+
npx @hrtips/cvx mcp init --client vscode # VS Code (.vscode/mcp.json)
183+
```
184+
185+
Then restart the client and ask it to make your CV — it fetches the schema, scaffolds, fills in your details, validates after every edit, and renders the PDF. The config writer merges into existing files; it never clobbers other servers. There's also a ready-made [Agent Skill](skills/cvx/SKILL.md) with the same loop for skill-capable agents.
186+
174187
### Your photo
175188

176189
Drop it into `cv-content/images/` as `profile.<ext>` — `jpg`, `jpeg`, `png`, or `webp` are auto-detected (that order wins if several exist). Square crop, at least 400×400px.

docs/ai-guide.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Pick the route that matches the tool you have:
1010
| A chat assistant with web access | [Route B](#route-b--chat-assistant-with-web-access) | One paste in, files out |
1111
| A chat assistant without web access | [Route C](#route-c--chat-assistant-self-contained-prompt) | Same, using a self-contained prompt |
1212
| An agent-mode assistant that can run commands (ChatGPT agent mode, …) | [Route D](#route-d--agent-mode-assistant-zero-local-setup) | Zero local setup — the assistant runs CVX in its own workspace |
13+
| An MCP client (Claude Desktop, Claude Code, Cursor, VS Code, …) | [Route E](#route-e--mcp-any-client-native-tools) | One-time config — the client gets native CVX tools |
1314

1415
Whichever route you take, the same two rules apply:
1516

@@ -144,6 +145,21 @@ The zip matters: agent workspaces are ephemeral, and your `cv-content/` folder i
144145

145146
---
146147

148+
## Route E — MCP: any client, native tools
149+
150+
CVX ships an MCP stdio server with four tools — `get_schema`, `init_cv`, `validate_cv`, `build_pdf` — thin wrappers over the same engine as the CLI. No API keys, fully offline; the server's instructions teach the model the loop and the truthfulness rules.
151+
152+
One-time setup (writes/merges the client's config, never clobbers other servers):
153+
154+
```bash
155+
npx @hrtips/cvx mcp init --client claude # Claude Code
156+
npx @hrtips/cvx mcp init --client claude-desktop # Claude Desktop
157+
npx @hrtips/cvx mcp init --client cursor # Cursor
158+
npx @hrtips/cvx mcp init --client vscode # VS Code
159+
```
160+
161+
Restart the client, then ask for your CV — e.g. *"Make me a CV from the LinkedIn text below. Use the CVX tools: fetch the schema, scaffold, fill in my real details, validate after every edit, and build both variants."* The assistant passes your workspace folder as `dir` on each call; the YAML lands in `cv-content/`, the PDFs next to it.
162+
147163
## Iterating with the assistant
148164

149165
Useful follow-up prompts once the first PDF renders:

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"name": "@hrtips/cvx",
33
"version": "1.3.0",
4-
"description": "CVX — structured input, professional output. YAML content in, pixel-perfect CV PDFs out; swappable themes and layouts, no headless browser. Formerly makecv.",
4+
"description": "CVX — structured input, professional output. YAML content in, pixel-perfect CV PDFs out; swappable themes and layouts, no headless browser. MCP server included. Formerly makecv.",
5+
"mcpName": "io.github.hrtips/cvx",
56
"bin": {
67
"cvx": "bin/cvx.js"
78
},
@@ -17,7 +18,10 @@
1718
"yaml",
1819
"cli",
1920
"ats",
20-
"react-pdf"
21+
"react-pdf",
22+
"mcp",
23+
"mcp-server",
24+
"model-context-protocol"
2125
],
2226
"scripts": {
2327
"dev": "vite",
@@ -38,7 +42,8 @@
3842
"lib",
3943
"bin",
4044
"template",
41-
"schema"
45+
"schema",
46+
"skills"
4247
],
4348
"devDependencies": {
4449
"@rollup/plugin-yaml": "^5.0.0",

server.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3+
"name": "io.github.hrtips/cvx",
4+
"description": "Create, validate, and render professional CV PDFs from plain YAML — fully local, no accounts. Tools: get_schema, init_cv, validate_cv, build_pdf.",
5+
"repository": {
6+
"url": "https://github.com/hrtips/cvx",
7+
"source": "github"
8+
},
9+
"version": "1.4.0",
10+
"packages": [
11+
{
12+
"registryType": "npm",
13+
"identifier": "@hrtips/cvx",
14+
"version": "1.4.0",
15+
"transport": {
16+
"type": "stdio"
17+
}
18+
}
19+
]
20+
}

skills/cvx/SKILL.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
name: cvx
3+
description: Create, validate, and render professional CV/resume PDFs from plain YAML using CVX — fully local, no accounts. Use when the user wants to write a CV or resume, convert an existing CV to a maintained format, tailor a CV for a job application, or produce an ATS-safe variant. Covers the cv-content/ YAML schema, the edit→validate→build loop, themes, layouts, and ATS keywords.
4+
license: Apache-2.0
5+
compatibility: Requires Node.js 20+ (runs via npx, no install). MCP server available via `npx @hrtips/cvx mcp`.
6+
metadata:
7+
author: hrtips
8+
homepage: https://github.com/hrtips/cvx
9+
---
10+
11+
# CVX — structured input, professional output
12+
13+
CVX renders a folder of plain YAML files (`cv-content/`) into a pixel-perfect CV PDF. Everything runs locally: no accounts, no network calls, and the user's data never leaves their machine. The YAML is the durable asset — the user keeps and re-edits it for every future application.
14+
15+
## The loop
16+
17+
If the CVX MCP server is connected, use its tools: `get_schema``init_cv` → edit YAML → `validate_cv``build_pdf` (pass the workspace folder as `dir`, absolute path). Otherwise use the CLI:
18+
19+
```bash
20+
npx @hrtips/cvx init # scaffold cv-content/ with a complete example CV
21+
npx @hrtips/cvx validate --strict --json # every problem at once: file + field paths + fixes
22+
npx @hrtips/cvx build --json # writes <name>.pdf; add --ats for the ATS-safe variant
23+
npx @hrtips/cvx list --json # available themes and layouts
24+
```
25+
26+
Exit codes: `0` ok · `2` validation failed · `3` render failed · `64` usage error. With `--json`, stdout is exactly one JSON object; logs go to stderr.
27+
28+
Always validate after every edit and before every build. Findings include the file, the field path, and a suggested fix — apply the fix and re-validate.
29+
30+
## Rules that are not optional
31+
32+
1. **Never invent facts.** Every entry must be truthful to the user's real history. This matters most for `keywords.yaml`: ATS parsers cross-check keywords against the CV body, and stuffing false terms gets CVs auto-rejected.
33+
2. **Don't rename the YAML files.** Sections are discovered by filename: `personal.yaml`, `summary.yaml`, `experience.yaml`, `education.yaml`, `competencies.yaml`, `achievements.yaml`, `referees.yaml`, `keywords.yaml`, `config.yaml`.
34+
3. **Quote strings containing colons** (`"Director: Operations"`). Date ranges are free text (`2019 – Present`).
35+
4. The photo goes at `cv-content/images/profile.jpg` (or `.jpeg`/`.png`/`.webp`) — ask the user for it; it cannot be generated.
36+
37+
## Content files (summary — the schema is authoritative)
38+
39+
Every scaffolded file carries a `$schema` header; the canonical JSON Schema lives at `schema/v1/` in the repo and is returned by the MCP `get_schema` tool.
40+
41+
- `personal.yaml` (object): `name` (required — drives the output filename), `title`, `company`, `phone`+`phoneHref`, `email`, `linkedin`+`linkedinHref`, `facebook`+`facebookHref`, `location`.
42+
- `summary.yaml`: list of 3–6 single-sentence bullets. A bullet may also be `{text, link: {href, label}, suffix}` to embed a clickable link (same form works in experience bullets).
43+
- `experience.yaml`: list of roles, most recent first — `role` (required), `company`, `period`, `location`, `description`, `progression` (list of `{title, period}`), `bullets` (verb-first, quantified, truthful).
44+
- `education.yaml`: list of `{degree, institution, period}`.
45+
- `competencies.yaml`: 6–12 short skill strings.
46+
- `achievements.yaml`: list of `{year, text}``year` is the bold headline (often the award name), `text` the attribution.
47+
- `referees.yaml`: list of `{name, title, company, email, phone}`, or `[]` for "available upon request".
48+
- `keywords.yaml` (optional): extra truthful ATS keywords not already covered by competencies/titles; embedded in PDF metadata, never printed.
49+
- `config.yaml`: `schemaVersion: 1`, `theme` (`teal`|`coral`|`mono`), `layout` (`two-column`|`single-column`|custom filename); pagination keys only if page 1 overflows.
50+
51+
## Variants
52+
53+
- Designed CV: `build` → two-column, photo sidebar, theme colours.
54+
- Job-portal upload: `build --ats` (or `build_pdf` with `ats: true`) → single column, no colours, machine-friendly, `<name>-ats.pdf`.
55+
56+
## When validation fails
57+
58+
Report the findings to the user in plain language, apply the suggested fixes to the YAML, and re-validate. Unknown keys are typos more often than not — the findings include a "did you mean" suggestion. Do not build until validation passes.

test/docsSync.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const schema = JSON.parse(readFileSync(path.join(ROOT, 'schema', 'v1', 'cvx.sche
1212
const cvSchemaDoc = readFileSync(path.join(ROOT, 'docs', 'cv-schema.md'), 'utf8')
1313
const scaffoldReadme = readFileSync(path.join(ROOT, 'template', 'cv-content', 'README.md'), 'utf8')
1414
const aiGuide = readFileSync(path.join(ROOT, 'docs', 'ai-guide.md'), 'utf8')
15+
const skillMd = readFileSync(path.join(ROOT, 'skills', 'cvx', 'SKILL.md'), 'utf8')
1516

1617
const CONTENT_DEFS = ['personal', 'summary', 'experience', 'education', 'competencies', 'achievements', 'referees', 'keywords', 'config']
1718

@@ -56,4 +57,17 @@ describe('scaffold README and AI guide stay aligned', () => {
5657
expect(aiGuide).toContain('validate')
5758
expect(aiGuide).toContain('schemaVersion')
5859
})
60+
61+
it('SKILL.md covers every content file, the MCP tools, and the truthfulness rule', () => {
62+
for (const def of CONTENT_DEFS) {
63+
expect(skillMd, `SKILL.md missing ${def}.yaml`).toContain(`${def}.yaml`)
64+
}
65+
for (const tool of ['get_schema', 'init_cv', 'validate_cv', 'build_pdf']) {
66+
expect(skillMd, `SKILL.md missing tool ${tool}`).toContain(tool)
67+
}
68+
expect(skillMd).toMatch(/[Nn]ever invent facts/)
69+
const [, frontmatter] = skillMd.split('---')
70+
expect(frontmatter).toContain('name: cvx')
71+
expect(frontmatter.length).toBeLessThan(1500)
72+
})
5973
})

0 commit comments

Comments
 (0)