tracker-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@tracker-mcpwhich follow-ups are due this week?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
tracker-mcp
A small MCP server over my job-search tracker. The tracker is an Excel file with about a hundred applications in it. Instead of opening the file, I ask Claude "which follow-ups are due this week?", "what's the status with Acme?" or "how many did I submit in October?". Claude can also record what happened: "mark row 12 as applied today via Ashby", "Globex rejected me".
The file stays the source of truth: I still open it in Excel, and other scripts edit it too. Most of the code is there to make writes safe in that setting.
Nothing about my particular tracker is in the code. The sheet, the column headers, the status words, the follow-up rule and the texts the server appends all come from a small TOML file next to the workbook (see Config).
Tools
Tool | Kind | What it does |
| read | How this workbook is laid out: fields → columns, allowed and closed statuses, follow-up rule |
| read | Filter by status, company/role text, submit dates |
| read | Every column of one application (by text or row) |
| read | Follow-ups due within N days, overdue first; submitted rows with no follow-up date |
| read | Still waiting, submitted N+ days ago, no follow-up planned ahead |
| read | Counts by status and month; a cohort for a date range |
| read | Audit log of this server's writes |
| write | New row with the not-applied status; other columns via |
| write | Submitted: date, channel, status, follow-up date by the configured rule |
| write | New status from the file's dropdown; a closed status cancels the pending follow-up |
| write | Log a sent follow-up, schedule the next one or close |
| write | Close all stale applications (or chosen rows) in one write, one backup, one undo step |
| write | Restore the backup of the last write if nothing changed since |
Read tools are annotated readOnlyHint; writes carry destructiveHint where they
overwrite cells. All write tools take dry_run, expected_version and confirm.
Related MCP server: JobTrack MCP Server
How a write stays safe
Lock file check. If Excel or LibreOffice has the file open (
~$…,.~lock.…#), the write is refused. Otherwise Excel's next save would overwrite it.Optimistic concurrency. Each read returns
version: a hash of the file's bytes. A write withexpected_versionis refused if the file changed since. It works like HTTPIf-Match.The user confirms, and the server enforces it. A "please ask first" line in the instructions only works if the model follows it, so the yes is checked on the server. Before the tool body runs, a resolver computes the plan, the exact cell changes (field, old → new) at the current file version:
If the client can show a dialog (MCP elicitation), the server asks the user there, with the plan as the message. Older protocol versions get a request in the middle of the call; 2026-07-28+ gets an input-required round. A decline writes nothing.
If the client has no dialog, the write needs
confirm: the token from a dry run of the same call. The token fingerprints the changes and the version, so a token from another preview, or from before the file changed, does not match.
Either way the body writes with the plan's version as If-Match, so what lands in the file is exactly what was approved, or nothing.
[safety] confirm_writes = falseturns this off.Write, verify, swap. Save to a temp file, re-open it and check every intended cell. Check the version once more, back up the current file, then
os.replace, which is atomic.Audit and undo. One JSON line per write: tool, row, changes, backup file. Undo works only if the file still matches the state right after that write, so it never wipes someone else's edit.
Validation comes from the workbook itself. Allowed statuses come from the column's dropdown. Columns are found by header text, not letter. When a row is added, the server extends the dropdowns, the autofilter and the summary sheet's formulas to cover it.
Expected refusals (a conflict, an unknown status, a duplicate) reach the model as a
readable tool error (ToolError). Any other exception counts as a crash, and the
SDK keeps its text out of the response.
Config
my-tracker.xlsx is described by my-tracker.tracker-mcp.toml next to it, or by
the file TRACKER_CONFIG points to. examples/tracker-mcp.example.toml documents
every key. In short:
sheet = "Applications"
[columns] # field = header text (exact, else the first header starting with it)
company = "Company" # company, role and status are required
role = "Role"
status = "Status"
applied_on = "Applied on"
followup_on = "Follow-up date"
followup_log = "Follow-up log"
salary = "Salary" # any other key is a custom field
[statuses]
not_applied = "Not applied"
applied = "Applied"
closed = ["Rejected", "Withdrawn", "No response", "Offer"]
[followups]
first_after_days = 10
next_after_days = 14
weekend_to_monday = trueA missing optional column only switches off what needs it: with no follow-up
column, followups_due says so instead of failing elsewhere. The config is
validated strictly, so a typo in a key is an error, not a silent default.
tracker-mcp --check prints how the config maps the workbook, and the server
refuses to start on a bad config, with the reason in the client's log.
My own tracker is in Russian; its config is tests/fixtures/ru.tracker-mcp.toml.
The tests run the same code against it and against an English layout with a
different sheet, header row and set of columns.
Known limits
The version covers the whole file, so any edit elsewhere invalidates a pending write. That's fine for one person; a shared tracker would need row-level versions.
A small window remains between the last version check and the rename. An xlsx file has no real locking.
openpyxl drops cached formula values. The server sets
fullCalcOnLoad, so Excel recalculates when the file is opened.Undo goes back one step only.
Without a dialog, the token proves that a preview was made and nothing drifted since. It cannot prove the model showed that preview to the user.
Run
Requires uv.
uv run pytest # tests on a synthetic workbook
TRACKER_PATH=~/path/tracker.xlsx uv run mcp dev src/tracker_mcp/server.py # MCP InspectorClaude Code runs the server with the Terminal's permissions, which is all it needs:
claude mcp add tracker -e TRACKER_PATH=/absolute/path/to/tracker.xlsx -- /bin/sh /absolute/path/to/tracker-mcp/run.shrun.sh exists because GUI apps on macOS don't get the shell PATH, so a bare
uv would not be found. Backups and audit.jsonl go to backups-tracker-mcp/
next to the tracker; TRACKER_BACKUP_DIR overrides that location.
Claude Desktop on macOS
Claude Desktop starts a local server as its own process, without Claude's privacy
permissions. A bare /bin/sh then cannot read ~/Documents, where my tracker
lives: macOS answers "Operation not permitted" and never shows a prompt. Full Disk
Access for Claude does not change that.
The fix is a small launcher app, ~/Applications/Tracker MCP.app (macos/launcher.c,
about 60 lines of C, signed locally). It starts the server as a child process. A
child inherits the process macOS holds responsible, so macOS asks once whether
"Tracker MCP" may access Documents. Only this app gets that permission, not Claude
as a whole. The launcher passes stdin/stdout straight through and forwards
termination signals. It also logs to stderr whether it runs in a sandbox and
whether it can read the tracker.
sh install.sh /absolute/path/to/tracker.xlsx does the whole setup: it syncs
dependencies, runs the tests, and builds and signs the launcher. Then it backs up
Claude Desktop's config and adds the server to it. Quit Claude first, because
Claude rewrites its config when it quits, and the script refuses to run while
Claude is open. The resulting entry:
{
"mcpServers": {
"tracker": {
"command": "/Users/you/Applications/Tracker MCP.app/Contents/MacOS/tracker-mcp",
"args": ["/bin/sh", "/absolute/path/to/tracker-mcp/run.sh"],
"env": { "TRACKER_PATH": "/absolute/path/to/tracker.xlsx" }
}
}
}Layout
src/tracker_mcp/tracker.pyholds all file logic and knows nothing about MCP.src/tracker_mcp/config.pyis the config schema (pydantic, strict) and loader.src/tracker_mcp/server.pyholds the MCP tools: type hints become the input schema, and each tool makes one call intoTracker.macos/holds the launcher for Claude Desktop:launcher.cand the bundle'sInfo.plist.tests/contains the domain tests on my Russian layout, the same logic on an English layout (test_config.py), and a test that drives the calls through an in-process MCP client.examples/tracker-mcp.example.tomlis a commented config to start from.
Built with the official MCP Python SDK (v2). MIT licensed.
This server cannot be deployed
Maintenance
Related MCP Connectors
Job search tracker for Claude and ChatGPT: applications, interviews, follow-ups, company watch.
- jobwyreOAuthapp.jobwyre
Track job applications on a board: let Claude or ChatGPT read and update jobs, interviews, notes.
Track job applications: find the ones to follow up on, add jobs, update status, notes and steps.
Analyze job listings against your resume, track applications, and generate cover letters.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables tracking job applications through a pipeline, scheduling follow-ups, and summarizing job search progress via natural language.7-
- FlicenseNot gradedqualityDmaintenanceManages job applications, companies, roles, statuses, deadlines, and reminders through natural language queries.-
- FlicenseAqualityBmaintenanceEnables managing a job search through natural language: tracking applications, discovery leads, interview prep, and resume generation. Connects to Claude via MCP to read and update local Excel files and documents.35-
- FlicenseNot gradedqualityCmaintenanceEnables AI agents to manage a personal job application tracker, allowing users to list, add, update, and delete job applications with company, role, and status details through natural language.-