cvgen — mcp server
MCP server
cvgen implements the Model Context Protocol, so an MCP client — Claude Code, Claude Desktop, Cursor, ChatGPT — can read and edit the resumes in your account. This page documents the endpoint, both authentication methods, and every tool, prompt and resource the server exposes.
- 13 tools
- 2 prompts
- OAuth 2.1
- API keys
01 / overview
What the server does
The server operates on the same records the editor does — there is no import or export step. An agent can list your resumes, read one, duplicate it, edit any part of the content or styling, manage folders, and write a cover letter.
There is one endpoint, and every request to it must be authenticated:
Thirteen tools, two guided prompts, three resources. The tools are grouped into three sets: resumes — listing, reading, creating, importing, duplicating, renaming, deleting, and one patch tool that handles every edit to a resume's body; folders — creating, renaming, deleting and moving between them; and discovery, for reading what the server supports before writing to it. The prompts drive the two flows worth scripting — building a resume from scratch and tailoring one to a job description. The resources hand the agent the JSON Schema for the resume shape and the list of templates, so it does not have to guess field names or template keys.
Streamable HTTP, stateless. The route serves POST and DELETE, responds with JSON rather than an SSE stream, and keeps no session between requests. Configure cvgen as Streamable HTTP, not SSE.
An open editor picks up agent edits on its own. Every write tool expires the cache entry behind the dashboard, so the new state is there the next time it loads. An editor left open on the patched resume polls for a newer timestamp and pulls the change in within about fifteen seconds, rather than quietly overwriting it with what was on screen.
02 / connect
Connecting a client
The endpoint is the same everywhere; only the place you paste it differs. Claude Code takes a CLI command, Cursor reads a JSON file, and Claude Desktop and ChatGPT register remote servers through their own settings UI — their connectors dial the endpoint from the vendor's cloud rather than from your machine, which is why there is no local file to edit.
Add the server, then authenticate from inside a session.
Run /mcp in Claude Code and pick cvgen. A browser opens, you sign in, you approve the request, and the token is stored for you. claude mcp list should then show cvgen as connected.
That command registers the server for the current project only. To have it everywhere, add the user scope:
03 / authentication
Authentication
Every request needs an Authorization: Bearer header. cvgen accepts two kinds of token and decides which is which by the prefix: a bearer starting with cvgen_live_ is verified as an API key, anything else is verified as an OAuth access token. There is no anonymous access and no read-only mode — a token that can read your resumes can also change them.
OAuth 2.1 — the default
Use this for anything with a browser: Claude Code, Claude Desktop, Cursor, ChatGPT. You never copy a credential, and you can revoke the grant later without touching your other clients.
- The client registers itself. cvgen supports dynamic client registration, so there is no developer portal and no client id for you to create.
- Your browser opens on cvgen's sign-in page if you are not already signed in.
- A consent screen names the client and the access it is asking for. You approve or decline.
- The client receives an access token bound to this endpoint as its audience, plus a refresh token so it does not have to ask again.
Clients that speak OAuth discover all of this on their own from two metadata documents — you should never need to enter them by hand:
API keys — for headless clients
A cron job, a CI step or a script on a server cannot complete a browser redirect. Generate a key in API key settings instead, then send it as the bearer token. Keys carry the same access as your session, so treat one like a password: keep it in an environment variable or a secret store, never in a committed file.
Claude Code takes the header as a flag:
Cursor and any other client that reads a JSON config take a headers object:
Deleting a key takes effect on the next request. The two paths are independent: revoking an OAuth grant leaves your keys working, and deleting a key signs no client out.
04 / tools
Tool reference
Thirteen tools, grouped by what they act on. Each one is scoped to the authenticated user — an id belonging to somebody else reads as missing rather than forbidden. Your client reads the full input schemas off the server, so the types below are a summary. A trailing ? marks an optional parameter.
Resumes
Everything that acts on a resume, from listing them to writing into one.
list_resumesLists the resumes and folders you own. The entry point — every other tool takes an id from here.
- folderId? string | null
folderId has three states: omit for everything, null for the root only, or an id for one folder.
get_resumeFetches one full resume, including content, customization and cover letter.
- id string
Read this before patching — the paths you patch are the paths you see here.
create_resumeCreates a resume prefilled with the default content and customization.
- title string
- folderId? string | null
It arrives with a full sectionsOrder, so the next step is patching values in, not building structure.
import_resumeCreates a resume from a complete content payload in one call — the MCP equivalent of the dashboard's Import Resume dialog, and it takes exactly the JSON that Export JSON produces.
- title string
- content object
- customization? object
- coverLetter? string
- folderId? string | null
The payload is validated against the same schema the UI import uses, so a malformed blob creates nothing.
duplicate_resumeDeep-copies a resume into the same folder as the source.
- id string
- title? string
The preview and profile images are copied as new objects, so the copy never mutates the original's files.
rename_resumeChanges a resume's title. Nothing else is touched.
- id string
- title string
patch_resumeThe only write path into a resume's body. Applies RFC 6902 JSON Patch operations to content and customization, and replaces the cover letter.
- id string
- contentOps? Operation[]
- customizationOps? Operation[]
- coverLetter? string
At least one of contentOps, customizationOps or coverLetter is required. Switching templates is more than replacing selectedTemplateKey — read list_templates in discovery first.
delete_resumeDeletes one resume, or a batch of them, and cleans up each one's preview and profile images.
- id string | string[]
There is no undo and no trash. Passing an array is the same delete the dashboard runs on a multi-select.
Folders
Organizing resumes. Folders are one level deep; there is no nesting.
create_folderCreates a folder. The slug is generated from the name if omitted.
- name string
- slug? string
rename_folderChanges a folder's name. The resumes inside it are untouched.
- id string
- name string
- slug? string
The slug is regenerated from the new name unless you pass one, so the folder's dashboard URL changes with it.
delete_folderDeletes a folder and everything filed in it.
- id string
Every resume inside the folder is deleted with it, permanently. Move anything worth keeping out first — there is no undo.
move_to_folderMoves one resume, or a batch of them, between folders — or to the root with null.
- id string | string[]
- folderId string | null
A move that would change nothing is an error, not a no-op — with an array, that means every id already sits in the target. The array form is the same move the dashboard runs on a multi-select.
Discovery
Read-only lookups for what the server supports, so a write is built from real keys rather than guessed ones.
list_templatesReturns every template with its key, title, theme keys, column count and default section order.
Read this before changing a resume's template: theme keys are per-template and the section keys differ between templates, so a swap has to rewrite sectionsOrder too. The same payload is available as the cvgen://templates resource.
Writing patches
Connected clients receive the full patching conventions from the server, so an agent does not need this. It matters if you are calling patch_resume yourself with an API key:
- Paths are rooted at the field.
contentOpspaths start insidecontent,customizationOpsinsidecustomization. Neither includes a leading/contentor/customizationsegment. - A patch is all or nothing. Operations are applied to a copy and validated against the schema before anything is written. If one path does not exist, nothing persists.
- Ids are assigned by the server. Omit
idwhen adding an item; address existing items by path (/experiences/2). Everything else the schema requires you still supply, notablyisVisibleon collection items.
Call get_resume first and build paths from what it returns.
What is not exposed
PDF and cover-letter downloads stay in the browser, because a PDF is bytes an agent cannot read — it would receive a file it can only hand back to you. Account, password, email and API-key management are deliberately out of reach of an MCP token: a token that can edit your resumes should not also be able to change who owns them. The built-in assistant keeps its own conversation history, and that is app state rather than resume data, so no tool reads or writes it. The line is drawn at your resumes and the folders they sit in.
06 / prompts
Prompts and resources
Two registered prompts. Each is a starting message that sets the order of operations for a flow; the conversation stays yours after it runs.
build_resumeFills a new resume section by section. It creates the resume, reads the template's sectionsOrder to learn which sections exist, then walks them one at a time, offering the optional sections only once the rest are done.
no arguments
tailor_resumeAdapts a resume to one posting. It duplicates the source first and works only on the copy, so the original is never edited. It asks clarifying questions, presents a change plan, and applies nothing until you confirm — then as one atomic patch. It will not invent skills, metrics, titles, dates or employers.
resumeId · jobDescription
Resources
Two resources expose the resume shape as JSON Schema, so an agent can check a field name instead of guessing at one. A third lists the templates:
The two schemas are the ones the app validates against, with one difference: every id is marked optional and annotated as server-assigned, which is what stops an agent inventing UUIDs.
07 / troubleshooting
Errors and what they mean
401 Unauthorized
Every rejected request looks the same, whichever token you sent. The response carries the header an OAuth-aware client needs to recover on its own:
A well-behaved client reads resource_metadata, discovers the authorization server from it, and starts the sign-in flow — so a 401 on first connect is normal and not a problem. A 401 that keeps repeating usually means one of these:
- No header at all. There is no anonymous access. If your client saved the server without completing sign-in, it is sending nothing.
- The access token expired and the client has no refresh token, usually because the grant was revoked. Re-run the sign-in flow — in Claude Code,
/mcp. - The token was issued for something else. Tokens are bound to this endpoint as their audience and to cvgen as their issuer. A token minted for a different resource fails verification, silently and by design.
- The API key was deleted. Keys are verified on every request; there is no grace period.
- The key was mangled in transit. A key must arrive as
Authorization: Bearer cvgen_live_…. Sent under any other header name, or without theBearerprefix, it is not seen at all.
The response never says which of these it was. It is always a bare Unauthorized, deliberately, so the endpoint cannot be used to probe which tokens exist.
405 Method Not Allowed on GET
The route answers POST and DELETE only, so aGET comes back 405. That is intentional and it does not stop the server working — responses are plain JSON, so no server-sent stream is needed. Configure cvgen as Streamable HTTP (some clients call it http, some streamable-http) and not as SSE.
Tool errors
Failures inside a tool come back as tool errors carrying a plain-text message, not as HTTP failures. A failed patch_resume writes nothing, so the resume is left as it was — call get_resume and rebuild the operations from what the server actually holds.