{"id":"389a2c9b-0799-4d55-8174-e578fb21abd3","slug":"callback","kind":"mcp","name":"callback","summary":"MCP server that scores your resume against a job posting, tailors it from your own evidence, and renders an ATS-ready PDF. Built on LangGraph. Never fabricates.","owner":"thedandano","repoUrl":"https://github.com/thedandano/callback","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-05-03T03:31:06.000Z","addedAt":"2026-10-21T04:32:03.482Z","logoUrl":"https://avatars.githubusercontent.com/u/47065799?u=29461736fab323a89a002d2d73ac82ddfa21d574&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["files-documents","cloud-infra","search"],"tags":["ai-agents","ats","automation","career","claude","job-application","job-hunting","job-search"],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-26T07:05:14.389Z","sample":false,"url":"https://conduits.dev/tools/callback","language":"Python","transports":["stdio"],"install":{"pypi":"callback"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# callback\n\n[![CI](https://github.com/thedandano/callback/actions/workflows/ci.yml/badge.svg)](https://github.com/thedandano/callback/actions/workflows/ci.yml)\n[![Latest release](https://img.shields.io/github/v/release/thedandano/callback)](https://github.com/thedandano/callback/releases)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE.md)\n[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org/downloads/)\n[![MCP compatible](https://img.shields.io/badge/MCP-compatible-brightgreen)](https://modelcontextprotocol.io/)\n\n### Your resume is probably being rejected by software, not by people.\n\nYou send out forty applications and hear nothing. Most of the time that silence isn't a hiring manager passing on you. It's a filter that scanned your resume for the words the posting asked for, didn't find enough of them, and sorted you below the line where a recruiter stops reading.\n\ncallback closes that gap without lying on your behalf.\n\nGive it a job posting. It scores your resume against that specific posting, tells you which keywords you're missing and what they're worth, rewrites your bullets using work you actually did, and hands back a finished PDF. It runs inside the AI assistant you already have open, so there's no new app to learn.\n\nRoughly how a session goes:\n\n```\nYou:      Tailor my resume for https://boards.example.com/senior-backend\n\ncallback: Score 58/100. Missing required: Kubernetes, Terraform, gRPC.\n          Found matching evidence in 3 of your stories.\n          Tailored 6 bullets. Rewrote 2 filler openers.\n          Score 81/100. PDF ready.\n```\n\n**It will not make things up.** Every rewritten bullet is built from evidence you gave it during setup. It won't invent a skill you don't have, and it won't pad your resume with keywords you can't back up in an interview. A resume that clears the filter and then falls apart on the phone hasn't helped you.\n\n**The score is real arithmetic, not a vibe.** Plain Python, no model in the loop. The same resume and the same posting always produce the same number, so when you change something you can tell whether it actually helped.\n\n**It admits what it can't see.** The score predicts whether a recruiter's keyword search surfaces you and whether your resume survives a ten-second skim. It doesn't know about work authorization, location, or job-title match, and it will never promise you an interview.\n\n---\n\n## Install\n\nYou need [uv](https://docs.astral.sh/uv/getting-started/installation/) on your `PATH`, Python 3.12 or newer, and your resume as a PDF, DOCX, or TXT file. That's it. The server downloads itself.\n\n### Claude Code\n\n```\n/plugin marketplace add thedandano/callback\n/plugin install callback@callback\n```\n\nRestart your session, or run `/reload-plugins`.\n\n### Codex\n\n```\ncodex plugin marketplace add thedandano/callback\ncodex plugin add callback@callback\n```\n\nRestart your session afterward.\n\n### Hermes Agent\n\n```\nhermes plugins install thedandano/callback\nhermes plugins enable callback\nhermes plugins list --plain --no-bundled\n```\n\nPortable plugins are installed disabled by default, so the `enable` step is required.\nStart a fresh chat afterward so Hermes connects the plugin's MCP server. `hermes\nmcp list` shows user-configured servers, not MCP servers bundled in portable\nplugins.\n\nPlugin skills are not shown by the `hermes skills list` command or added to the\nsystem prompt. In a chat, ask Hermes to call `skills_list`; it returns the full\ngenerated names, such as `agent-plugin-callback-<hash>:tailor-resume`. Load one\nwith `skill_view(\"<qualified-name>\")`. Do not assume the namespace is\n`callback:`.\n\nClaude, Codex, Hermes, and future plugin-capable hosts share the same `skills/`\ndirectory. Only their discovery names and MCP launch adapters differ.\n\n### Cursor, Claude Desktop, or any other MCP client\n\nPaste this into the client's MCP config. `uvx` fetches, builds, and runs the server straight from GitHub, so there's nothing to install first.\n\n```json\n{\n  \"mcpServers\": {\n    \"callback\": {\n      \"command\": \"uvx\",\n      \"args\": [\n        \"--from\",\n        \"git+https://github.com/thedandano/callback\",\n        \"callback\",\n        \"serve\"\n      ]\n    }\n  }\n}\n```\n\nRestart the client.\n\nGive the first launch a minute. `uvx` downloads the server and its dependencies, then the server quietly installs a headless Chromium browser that it uses to read job postings and render PDFs. Every launch after that is quick.\n\n<details>\n<summary>Prefer the standalone CLI?</summary>\n\nYou don't need this for any of the installs above. Use it if you want the `callback` command in your own shell, for scripting or for a client without plugin support.\n\n```bash\nuv tool install git+https://github.com/thedandano/callback.git\ncallback install-browsers      # headless Chromium, for reading postings and rendering PDFs\n```\n\nRegister `callback` with your MCP client the normal way for that client (e.g. add it to `~/.claude.json` or `~/.codex/config.toml`), then restart the client.\n\nRun `callback config status` to confirm your settings. It's read-only and shows the contents of `~/.config/callback/env.json`, the file callback itself owns for tracing and other overrides — no MCP host config file needed.\n\n</details>\n\n> Not on PyPI yet. Installing from the GitHub URL is the supported path for now.\n\n---\n\n## First run\n\n### Set up your profile\n\ncallback can't tailor anything until it knows about you. In your assistant, ask for it in plain English:\n\n```\nSet up callback for my job search\n```\n\nThat runs the `setup-callback` skill, which registers your resume, builds your profile, and captures what you're looking for. You do this once.\n\nYour profile is a set of behavioral stories: short write-ups of things you actually did, in Situation / Behavior / Impact form. That's the evidence callback pulls from when it rewrites a bullet, and it's the reason the tool can tailor without inventing anything. Setup will walk you through your first few, and you can add more whenever you think of them.\n\n### Tailor your first resume\n\n```\nTailor my resume for https://example.com/careers/some-job\n```\n\nYou get a scored gap report and a tailored PDF; each run is also recorded in callback's ledger (`~/.local/share/callback/ledger.db`). By default PDFs land in `~/.local/share/callback/applications/`.\n\nIf your assistant runs in a sandbox that can't reach your home directory, tell it where to put the PDF instead and it will pass that path through.\n\n---\n\n## Everyday use\n\nAsk for these in plain English or use the slash command.\n\n| Skill | Use it to |\n|---|---|\n| `setup-callback` | Set up everything in one pass: paths, resume, preferences. Start here. |\n| `onboard-profile` | Register a resume, add accomplishments, or write a new story. |\n| `tailor-resume` | Score and tailor for one job posting. |\n| `export-ledger` | Export the ledger to CSV, optionally by status or date. |\n| `scan-job-leads` | Find and dedupe job leads. Doesn't apply to anything. |\n| `apply-to-job` | Score, tailor, stage, and record an application. |\n\nActually submitting an application always needs your approval in the moment. callback won't send anything on your behalf.\n\n---\n\n## How the score works\n\nFive dimensions, 100 points. Each one stands in for a real gate your application has to clear.\n\n| Dimension | Max | What it measures |\n|---|---|---|\n| KeywordMatch | 55 | Required keywords (weighted 0.7) and preferred ones (0.3). Whether a recruiter's search finds you at all. |\n| ExperienceFit | 15 | Whether you meet the stated years of experience. Skipped and renormalized when the posting doesn't say. |\n| ImpactEvidence | 10 | How many bullets carry a real, quantified result. |\n| ATSFormat | 10 | Standard section headings, so the parser doesn't mangle your resume. |\n| Readability | 10 | Absence of filler like \"responsible for\" and \"worked on\". |\n\nPassing mark is 70. Weights live in `ScoringConfig` in `callback/scorer.py` if you want to argue with them.\n\nThe number is worth what it's worth. It can't see your work authorization, your location, whether your job title matches the req, how recent your skills are, or degree and clearance filters. Those account for a lot of automatic rejections and callback has no visibility into any of them. Read the score as \"will I survive the search and the skim\", not \"will I get called\".\n\n---\n\n## What happens to your resume\n\ncallback makes three kinds of outbound network calls. It fetches the job posting from the URL you give it. It checks the GitHub releases API for updates. And if you turn on LangSmith tracing, which is off by default, it sends traces to your own LangSmith project. Those traces include the job URL, the posting text, the extracted keywords, your submitted edits, and output paths. They also include the resume content callback hands to your client for tailoring: resume bullets, the wiki index, project previews, and any wiki pages your client asks for (including story text). Email addresses and phone numbers are masked, the contact block of your resume is replaced with placeholders, and the compiled profile and the full resume sections block are reduced to counts.\n\nScoring, parsing, and PDF rendering all happen on your machine. callback never calls an AI model itself.\n\nThe part worth understanding: callback hands your resume sections to your MCP client so that client's model can write the tailored bullets. If you're using a cloud assistant, your resume content goes to that provider under their terms, the same as if you'd pasted it into the chat window. That's how the tool works, and you should know it before you point it at your resume.\n\nYour profile, applications, and history live in `~/.local/share/callback/`.\n\n---\n\n## Optional: tracing with LangSmith\n\ncallback runs fine without this. Turn it on if you want to watch the graph execute.\n\n```bash\ncallback config langsmith                                  # guided setup\ncallback config langsmith --api-key lsv2-... --project Callback   # noninteractive\n\ncallback config status            # inspect; secrets redacted by default\ncallback config env list --show-secrets\n\ncallback config env set CALLBACK_TRACE_BACKEND langsmith\ncallback config env unset LANGSMITH_API_KEY\n\ncallback trace-check --emit-test-trace      # verify it works\n```\n\nThese commands write to `~/.config/callback/env.json` (override the location with `XDG_CONFIG_HOME`), a settings file callback owns and reads itself at startup — independent of how you installed it (plugin, standalone CLI, or a hand-edited MCP config). Restart your MCP client after any change so it picks up the new environment.\n\n<details>\n<summary>Tracing details</summary>\n\nSet `CALLBACK_TRACE_BACKEND=langsmith`, `LANGSMITH_TRACING=true`,\n`LANGSMITH_ENDPOINT=https://api.smith.langchain.com`, `LANGSMITH_API_KEY=<key>`, and\noptionally `LANGSMITH_PROJECT`, which defaults to `Callback`.\n\nTwo layers are in play: LangGraph `RunnableConfig` metadata for graph runs, and sanitized\ndecorator spans for MCP tool calls and graph nodes.\n\nThe server suppresses native LangChain and LangGraph auto-tracing around graph invocation,\nbecause callback deliberately pauses its graphs to hand control back to the MCP client.\nWithout that suppression, LangSmith shows a graph run stuck as pending while it waits for\nthe next tool call. The sanitized `callback.*` spans are the supported surface.\n\nTool spans carry the tool's main arguments and its reply. Emails, phone numbers and the\nresume contact block are masked, and the `sections` and `compiled_profile` blocks are\nreduced to counts. Resume bullets offered for trimming, the wiki index, project previews\nand any wiki pages your client requests are sent as text. Node spans carry only lengths,\nkey names, booleans and identifiers. Where the code chose a path, the span also carries a\nshort label saying which, such as `jd_source=pasted` or `jd_truncated=true`. API keys are\nnever sent.\n\n</details>\n\n---\n\n## Maintenance\n\n```bash\ncallback logs --follow          # default: ~/.local/state/callback/server.log\ncallback update                 # upgrade via uv tool upgrade\n```\n\n`callback logs` also takes `--project-logs`, which reads `./.callback/server.log`, or\n`--log-path <path>`.\n\nTo bring older job-hunt records into the ledger once (safe to run again; your source files are\nonly read):\n\n```bash\ncallback ledger import --archives <folder> --csv <record.csv> --contacts-db <old.sqlite3>\n```\n\nEach flag is optional, and `--archives` can be repeated.\nThe command reports imported jobs and status events separately for each source.\n\nTo uninstall:\n\n```bash\ncallback uninstall              # remove MCP server entries from Claude and Codex\nuv tool uninstall callback      # remove the tool\n```\n\nAdding `--purge` to the first step also deletes `~/.local/share/callback/`, which holds your\nPDFs and the ledger, along with `~/.local/state/callback/` for logs. Session databases live in the temp folder, not there.\nYou can't undo that.\n\n---\n\n## Scheduling a recurring scan\n\nPoint a scheduler at the `scan-job-leads` skill.\n\nClaude `/schedule`, weekday mornings:\n\n```\n/schedule \"0 8 * * 1-5\" Use the scan-job-leads skill. Read search preferences via get_search_preferences, scan configured sources within lead_recency_days, dedupe every lead with find_job against callback's ledger, and return the standard scan-job-leads summary with recommended leads for apply-to-job.\n```\n\nOr plain cron:\n\n```cron\n0 8 * * 1-5  claude --print \"Use the scan-job-leads skill.\" >> ~/job-scan.log 2>&1\n```\n\nAfterward, run `apply-to-job` on the leads you want. Submitting still needs your approval.\n\n---\n\n## MCP tools\n\nThe skills above cover normal use. These are the tools underneath, if you want to build your\nown flow.\n\n| Tool | Description |\n|---|---|\n| `load_jd` | Fetch a job posting from a URL, or accept raw text. Returns the posting as markdown plus extraction instructions. |\n| `submit_keywords` | Accept the client-extracted job data: title, required and preferred keywords, seniority, years. Scores the resume and returns keyword gaps. |\n| `submit_responsibilities` | Optional second pass: send the job's responsibilities as short keywords, for tailoring. Not scored. |\n| `submit_tailor` | Apply bullet edits, render the PDF, score the result, produce the report. |\n| `get_wiki_pages` | Fetch pages from your behavioral-story profile by path. |\n| `onboard_user` | Register a resume, skills file, and accomplishments doc. |\n| `compile_profile` | Recompile your profile from all stored stories. |\n| `create_story` | Save a new behavioral story for a skill. |\n| `set_search_preferences` | Save your job-search preferences. |\n| `get_search_preferences` | Read your job-search preferences. |\n| `check_update` | Report the current version, the latest release tag, and whether an update is available. |\n| `find_job` | Check whether a job is already in the ledger, its stored URL, latest status, `ever_applied`, and score totals (dedupe). |\n| `record_status` | Add a status (Applied, Rejected, ...) to a job in the ledger; never overwrites history. A non-http(s) `url`, like a recruiter thread, goes into the note. |\n| `export_ledger` | Write the ledger as CSV to an absolute path: one row per job (latest status and scores), with `mode=\"events\"` one row per status event with its own date, or with `mode=\"contacts\"` only the EDD-reportable events (notes starting `Contact method:`, statuses starting `Applied` or `Already applied`). |\n\nThe client owns keyword extraction and tailoring judgment. callback owns state, validation,\nscoring, rendering, and archival. The full handoff protocol is in\n[CLAUDE.md](CLAUDE.md#agent-mcp-playbook).\n\n---\n\n## Development\n\n```bash\ngit clone https://github.com/thedandano/callback.git\ncd callback\nuv sync\nuv run playwright install chromium     # one-time\n\nuv run pytest                          # tests\nuv run pyright                         # type check\nuv run ruff check                      # lint\nuv run python -m callback.server       # run the server locally\n```\n\nFor a development install that puts the CLI on your `PATH`:\n\n```bash\nmake install\n```\n\nThen register `callback` with your MCP client the normal way for that client (e.g.\nadd it to `~/.claude.json` or `~/.codex/config.toml`), same as the standalone CLI\ninstall above.\n\n`make install` embeds a build version derived from git. A clean checkout of `origin/main`\nprints the package version. Commits ahead of it get a suffix like `1.1.0-03-a1b2c3d`, and\nuncommitted changes add `-dirty`.\n\nArchitecture, graph design, and change discipline are documented in\n[CLAUDE.md](CLAUDE.md) and [BRIEF.md](BRIEF.md).\n\n### Model evals\n\nThe runner sends each saved job or resume example to a model, saves its reply,\nthen runs the extraction or tailoring checks. It tests those answers directly;\nit does not run the full MCP workflow or fetch live job postings.\n\n```bash\nuv run python scripts/run_evals.py --host claude       # saved/default model\nuv run python scripts/run_evals.py --host codex        # saved/default model\nuv run python scripts/run_evals.py --host hermes       # saved/default model\nuv run python scripts/run_evals.py --host hermes --provider openrouter --model MODEL_ID\nuv run python scripts/run_evals.py --host ollama --model MODEL_NAME\nuv run python scripts/run_evals.py --host llamacpp --model MODEL_ALIAS\n```\n\nReplace the capitalized names with your model or server alias. `--model` is\nrequired for Ollama and llama.cpp. Start the local server and load the model\nfirst; installing the CLI alone is not enough. Ollama defaults to port `11434`,\nand llama.cpp defaults to `8080`. Override either port with `--port`:\n\n```bash\nuv run python scripts/run_evals.py --host ollama --model MODEL_NAME --port 12345\nuv run python scripts/run_evals.py --host llamacpp --model MODEL_ALIAS --port 8081\n```\n\nBoth local targets use their OpenAI-compatible chat endpoint on `127.0.0.1`.\nFor llama.cpp, use the model alias set when starting `llama-server`.\n\nFor Claude, Codex, and Hermes, `--model` overrides the harness default. Without\nit, the runner reads the saved model name from the harness's user settings, then\npasses that name to the isolated CLI. It respects `CLAUDE_CONFIG_DIR`,\n`CODEX_HOME`, and `HERMES_HOME`; Claude's `ANTHROPIC_MODEL` and Hermes's\n`HERMES_INFERENCE_MODEL` environment overrides also apply. With no saved choice,\nthe runner leaves model selection to the CLI. It does not copy other user\nsettings, Codex profile overrides, or custom provider definitions. Hermes follows\nits active profile, including an explicit profile directory in `HERMES_HOME`.\n\nHermes also reads the provider from its saved `model` settings; `--provider`\noverrides that choice. Authentication stays with the harness. Hermes uses\n`--safe-mode` to exclude personal rules, memory, plugins, and MCP connections,\nbut may retain built-in tools. Its results measure the harness as well as the\nmodel; direct Ollama and llama.cpp calls have no tools. Use a Hermes version\nsupporting `--query-file`, `--safe-mode`, and `--format stream-json`.\n\nRun one example or check saved replies without calling a model:\n\n```bash\nuv run python scripts/run_evals.py --host ollama --model MODEL_NAME --eval extract --case reddit\nuv run python scripts/run_evals.py --checks-only\n```\n\nThe runner logs before each host call. Each fresh run overwrites that example's\n`host.json`; failed calls replace stale replies with a recorded error. Runs are\nsequential, and simultaneous runs would overwrite each other's files. Local\nport and Hermes provider settings are saved with the results and sent to\nLangSmith experiment metadata. `--no-langsmith` skips experiment uploads;\n`--checks-only` always stays offline and needs no model.\n\n---\n\n## License\n\n[MIT](LICENSE.md).\n\n---\n\nIf callback got you past a filter, star the repo. That's the easiest way to help the next\nperson find it. Bugs and ideas go in [issues](https://github.com/thedandano/callback/issues).\nForks and pull requests welcome.\n"}