workspace-status-mcp
You can monitor git repository status, check architecture documentation freshness, detect release drift, and write docs with commit tracking.
sweep_status: one-call snapshot of all git repos under a root (or multiple roots), showing branch, uncommitted changes, unpushed commits, and optionally latest CI status; can filter to only repos needing attention.check_docs: flags which repos'Architecture/<repo>.txtdoc is missing, stale, current, or no-commits; uses commit-based tracking if written viawrite_doc, else mtime; supports multiple projectsRoot+docsRoot points.check_release_drift: for explicit source→release repo pairs, counts commits since the release's last tag and the age of the oldest unreleased commit; highlights drift.write_doc: writesArchitecture/<repo>.txtand records the repo's HEAD commit hash, enabling precise staleness tracking; does not generate content.
Provides visibility into local Git repositories, reporting branch, uncommitted changes, unpushed commits, and commit drift between source and release repos.
Queries the latest GitHub Actions CI run status and conclusion for repositories, making it easy to spot failing or non-success CI runs.
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., "@workspace-status-mcp Sweep all repos in ~/Projects for git and CI status"
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.
workspace-status-mcp
An MCP server with six tools:
sweep_status— a one-call snapshot of every git repository under a given folder: branch, uncommitted changes, unpushed commits, and (optionally) the latest GitHub Actions CI conclusion. Replaces manually loopinggit status+gh run listover dozens of repos one at a time.check_docs— flags which repos' docs (<repo>/.arch/, see Doc layout) are missing or stale. Doesn't write or regenerate anything itself (understanding a codebase well enough to document it is an LLM/human job, not a script's) — it just says where to look, so docs get updated deliberately instead of silently rotting.write_doc— writes one doc file (<repo>/.arch/<file>) and stamps it with the repo's current commit hash, socheck_docscan later measure staleness precisely (commits since write) instead of guessing from file mtime.check_release_drift— for explicit (source repo, release repo) pairs, counts how many commits landed in the source since the release repo's last git tag, and how old the oldest one is. Cutting a release is usually a manual "whenever I remember" step (tag a version, push it, CI builds and publishes) — this answers "has anyone actually done that lately" without checking by hand.read_doc— reads a repo's docs in PARTS instead of whole files: no arguments → the list of files in.arch/plus a numbered table of contents ofREADME.txt;file→ another file;section(number or title fragment) → one section;query→ only the matching lines with context (across ALL files unlessfileis given). Recognises underlined,##, ALL-CAPS, dated-entry and--- entry ---/=== entry ===/[YYYY-MM-DD]headings.check_pr_status— a one-call snapshot of several GitHub PRs at once: state, mergeable, review decision, CI status, and — separately — how many top-level and inline review comments each has, with the latest author/timestamp of each kind. Top-level (issue) comments and inline (review) comments are two genuinely different GitHub API resources; a PR reviewed with inline comments only can look untouched if you check just the review body. Replaces loopinggh pr view+ two separategh api .../commentscalls per PR.
Why
Working across ~50 repositories in the same workspace, "what actually needs
attention right now" was a real recurring question — checked by hand,
repo by repo, over and over in the same session. sweep_status answers it
in one call and, by default, only returns repos that actually need a look
(dirty working tree, unpushed commits, or a CI run that isn't a plain
success) — clean repos are silently skipped so the answer stays short.
check_docs exists for the same reason, one level up: a per-project
architecture doc is only useful if it's trusted, and it's only trusted if
someone actually checks it's current. Comparing "last commit" to "doc's
mtime" turns that from a thing you have to remember into a thing you can
just ask.
Related MCP server: devagent
Doc layout: .arch/ and backup
Since v0.7.0 each repo keeps its docs inside itself, in a git-ignored
.arch/ folder, instead of one big <repo>.txt in a central folder (the big
ones had grown to 1500 lines / 150 KB, unreadable as one file):
<projectsRoot>/<repo>/.arch/
README.txt overview: what it is, stack, layout, list of the other files
<topic>.txt topic files for big projects (e.g. usb.txt, disk-installer.txt)
changelog.txt dated entries ("what changed") — new ones are appended here
.meta.json {commitHash, writtenAt} written by write_docAdd
.arch/to your global gitignore (~/.config/git/ignore) so it never lands in a (public) repo.Backup. Because
.arch/is ignored, nothing else keeps a copy of it.bin/arch-backup.mjs <projectsRoot> <backupDir>copies every<repo>/.arch/into a separate (private) git repo as<backupDir>/<repo>/…, commits and pushes. It only ever adds/updates — never deletes: a lost.arch/(or a whole deleted project) stays in the backup. Symlinks are not followed. A lock file stops two runs from colliding; pushes are throttled to once per 30 min unless--push-now.It runs automatically from two places: the
check-docs-reminderhook on every Claude CodeStop(detached, when<projectsRoot>/Architecture-backup/.gitexists), and a global gitpre-pushhook (hooks/git/dispatch.sh, installed byhooks/git/install.shascore.hooksPath) — so everygit pushof any repo also pushes the docs backup. The dispatcher first runs the repo's own.git/hooks/<name>if there is one (settingcore.hooksPathwould otherwise disable it) and never blocks the push.The old central layout still works: pass
docsRootexplicitly (tool argument or watch point) and<docsRoot>/<repo>.txt+<docsRoot>/.meta/<repo>.jsonare used exactly as before.
Claude Code hook: check-docs-reminder
The tools above only help if something actually calls them. hooks/check-docs-reminder.mjs
closes that gap: registered as a SessionStart + Stop hook in
~/.claude/settings.json, it runs checkDocs() itself against whatever
repo Claude's current working directory is under (walking up to the
nearest git root that's a direct child of the projects folder), and — only
when that repo's doc is missing or stale — injects a one-line reminder
into Claude's context via hookSpecificOutput.additionalContext. Silent
otherwise (clean repos, or a cwd outside the projects folder, produce no
output; likewise if no repo under the projects folder has a .arch/ yet
(or, for a point with an explicit docsRoot, that folder doesn't exist) — someone who's never opted into this convention doesn't get nagged
about every repo being "missing"). SessionStart covers forgetting between
sessions; Stop (which fires each time Claude's turn ends) re-checks every
turn within the same session too, and self-quiets the moment write_doc
actually gets called. Deliberately non-blocking — a stale doc is worth a
nudge, not a halted turn.
Not hardcoded to any one person's folder layout, and works the same on
Windows as Linux/macOS. Register it with the args array ("exec form" —
spawned directly, no shell involved, so there's no bash-vs-PowerShell-vs-cmd
syntax difference to worry about):
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "node",
"args": ["/path/to/workspace-status-mcp/hooks/check-docs-reminder.mjs", "SessionStart"] }] }],
"Stop": [{ "hooks": [{ "type": "command", "command": "node",
"args": ["/path/to/workspace-status-mcp/hooks/check-docs-reminder.mjs", "Stop"] }] }]
}
}Watch points
Not every repo necessarily lives under one root — one might get moved out
of ~/Projects onto the Desktop, say, with its doc sitting right next to
it there. The hook
resolves "watch points" (projectsRoot + optional docsRoot pairs) in
this order, using the first point whose projectsRoot contains the repo
you're currently in:
CLI args (
args[2]/args[3]after the event name) — a one-off single-point override.The watch-points config file —
~/.claude/workspace-status-points.jsonby default (override the path withWATCH_POINTS_FILE). This is the normal way to manage this day to day:{ "points": [ { "projectsRoot": "/home/sviat/Projects" }, { "projectsRoot": "/home/sviat/Desktop", "docsRoot": "/home/sviat/Desktop" } ] }Add a point, remove one (or all of them), or redirect an existing one just by editing this file — no code change, no re-registering the hook. A missing file, or an empty/absent
pointsarray, falls through to the next source below.PROJECTS_ROOT/DOCS_ROOTenvironment variables — single-point fallback for anyone registering the hook through a shell command instead of theargsexec form.Default: a single point at
<home>/Projects.
check_docs/sweep_status accept the same idea directly as points/roots
arguments (see below) if you want to query multiple locations from a
conversation without touching the config file.
Install
npm installRequires the GitHub CLI (gh), authenticated, if you want CI status
(check_ci: true, the default). Without it CI results just come back as
null per repo.
Cross-platform — all six tools and the hook are plain Node.js (path.join,
os.homedir(), no hardcoded /) shelling out to git/gh, both of which
run natively on Windows too. No platform-specific code path.
Updating
There's no separate build or publish step — claude mcp add points
straight at this checkout's src/server.js, so updating is just:
git pull && npm installTake effect on the next new Claude Code session (each session spawns its own MCP server process, so an already-running session keeps using the code it started with).
Tool: sweep_status
Argument | Type | Default | Meaning |
| string | — (required unless | Folder to scan, one level deep (e.g. |
| string[] | — | Several roots in one call instead of one |
| string[] | all subfolders | Limit to specific repo names instead of scanning everything (matched across all roots) |
| boolean |
| Also query GitHub Actions for each repo's latest run |
| boolean |
| Only return repos that need a look; |
Each repo entry: name, path, branch, uncommittedFiles (count),
ahead/behind (vs upstream, null if no upstream configured),
hasUpstream, and ci ({status, conclusion, workflow, url} or null).
Tool: check_docs
Argument | Type | Default | Meaning |
| string | — (required unless | Folder with the repos |
| string | — (in-repo | Old central layout only: folder with |
|
| — | Several independent projectsRoot+docsRoot pairs in one call instead of one |
| string[] | all subfolders | Limit to specific repo names (applied within each point independently) |
| boolean |
| Only return missing/stale; |
Each repo entry: name, projectsRoot (which point it came from),
docPath, status (missing / stale /
current / no-commits), trackingMethod (commit if the doc was
written via write_doc, mtime otherwise — see below), lastCommitAt,
and either commitsSinceWrite/writtenAtCommit/writtenAt (commit
tracking) or docUpdatedAt/staleBySeconds (mtime tracking, only on
stale).
Tool: write_doc
Argument | Type | Default | Meaning |
| string | — (required) | Folder with the repos |
| string | — (required) | Repo folder name (e.g. |
| string | — (required) | With |
| string |
| File inside |
| string | — (in-repo | Old central layout only: folder with |
|
|
|
|
Writes <repo>/.arch/<file> and <repo>/.arch/.meta.json with the repo's
HEAD commit hash at write time. check_docs then reports the exact
number of commits since the doc was written (git rev-list --count)
instead of the coarser mtime-vs-last-commit-time comparison — the same
pattern check_release_drift already uses for source→release drift. Docs
written directly (e.g. via a plain file write, not this tool) keep using
mtime tracking — there's no meta file to compare against.
mode:"append" exists for one specific case: adding a single new dated
entry to an already-large (hundreds/thousands of lines) doc file without
first reading the whole file into context and sending it back unchanged
just to append one paragraph — that pattern used to waste a lot of
tokens for no reason.
Tool: check_release_drift
Argument | Type | Default | Meaning |
| string | — (required) | Folder with the repos |
|
| — (required) | Explicit list of source→release folder-name pairs |
| boolean |
| Only return |
Each pair entry: source, release, status (drifted / current /
no-tags), and on drifted: latestTag, tagCreatedAt,
commitsSinceTag, oldestUnreleasedCommitAt, oldestUnreleasedAgeDays.
Tool: check_pr_status
Argument | Type | Default | Meaning |
|
| — (required) | PRs to check, |
| boolean |
| Only return PRs that are |
Each entry: title, url, state, mergedAt, mergeable,
mergeStateStatus, reviewDecision, ciStatus (success / failure /
pending / none), comments ({total, last: {author, at} | null} —
top-level issue comments), reviewComments (same shape, inline review
comments), needsAttention. A PR that fails to fetch (bad repo/number)
gets {repo, number, error} instead of throwing and losing the rest of
the batch.
classifyPr() in src/pr-status.js is the pure decision logic (given
already-fetched raw data, no network) — unit-tested with fixtures
separately from the real gh calls, which are verified against actual
merged PRs instead.
Architecture
src/sweep.js— all the logic: finds.gitfolders one level under a root (exported asfindGitRepos, reused bydocs.js), then for each one runsgit branch/git status/git rev-listand (optionally)gh run listin parallel, batched at 8 repos at a time to avoid hammering the GitHub API.sweepStatus()'sroots(plural) runsfindGitReposper root and merges the results before filtering byrepos/only_attention— so a repo name filter matches regardless of which root it actually lives under.src/arch.js— where a repo's docs live (locate(): in-repo.arch/or the old centraldocsRoot), which files they consist of (listDocFiles(), README first, changelog last) and the file-name guard.src/docs.js—checkDocs(): prefers commit-based tracking (.meta.json, written bywrite_doc) when available; falls back to comparinggit log -1 --format=%ctagainst the newest doc file's mtime for docs written directly. SkipsArchitecture/Architecture-*folders (the doc stores themselves). A repo with no commits yet reportsno-commitsrather than being silently lumped intomissingorcurrent.points(plural) checks each independent projectsRoot+docsRoot pair in turn and tags every result with which one it came from.src/write-doc.js—writeDoc(): writes one.arch/<file>plus.arch/.meta.json({commitHash, writtenAt},HEADat write time). Doesn't generate the text itself — understanding a codebase well enough to document it stays an LLM/human job.src/pr-status.js—checkPrStatus(): fetchesgh pr viewplus both comment endpoints (issues/{n}/commentsfor top-level,pulls/{n}/commentsfor inline review comments — deliberately separate, since a PR reviewed only with inline comments looks untouched if you check just the review body) per PR, batched at 6 at a time.classifyPr()is the pure part (decidesneedsAttention, picks the latest comment of each kind) — no network, so it's tested with fixtures independently of the realghcalls.src/server.js— registers all six tools with the MCP SDK over the stdio transport.test/smoke.mjs—sweep_statusagainst real local repos for the clean/dirty cases (no synthetic fixtures needed — the workspace itself already has both to test against), a throwaway local git repo for the no-upstream case (an incidental empty folder under~/Projectsused to serve this, until it got renamed away mid-session and silently broke the test — not something to depend on again), plus aroots(plural) case against a real root and a throwaway empty one.test/pr-status.mjs—classifyPr()against fixture data (clean/DIRTY/ CHANGES_REQUESTED/CI-failure/CI-pending, comments counted separately per kind, a merged PR never flagged regardless of leftoverDIRTYstate), pluscheckPrStatus()against real, permanently-merged PRs in two different repos (mergedAt/statewon't change), a multi-PR batch call, and a nonexistent PR number returning{error}in its own entry instead of failing the whole batch.test/docs.mjs—check_docsagainst temporary, throwaway git repos with controlled commit/file timestamps (real~/Projectsdrifts over time, which would make a fixed test flaky), including both tracking methods and apoints(plural) case.test/write-doc.mjs—writeDoc()against a temporary git repo: correctHEADcaptured,.meta/created on demand, empty content rejected.test/hook.mjs—hooks/check-docs-reminder.mjsas a real subprocess (it's a CLI entry point, not a library function): silent outside any watch point, silent when no repo has.arch/yet, reminds and resolves the right repo from a nested subdirectory, the watch-points config file driving two independent points at once, and an empty/absentpointsarray in that file falling through to the next source, andStopspawning the backup.bin/arch-backup.mjs— the backup (see Doc layout).hooks/git/dispatch.sh+install.sh— the global git hooks.test/arch-backup.mjs— backup in throwaway folders only: copies and pushes to a local bare remote, never deletes (lost.arch/and a deleted project both survive and restore), ignores symlinks, lock and push throttling, and the realpre-pushdispatcher end-to-end (local hook still runs; a failing local hook still blocks the push).src/release-drift.js—checkReleaseDrift(): finds the release repo's most recent tag viagit for-each-ref --sort=-creatordate(sorted by actual tag time, not the semver-string sort-v:refnamewould give —v1.10would otherwise sort beforev1.9), then countsgit log --since=@<tagTimestamp>in the source repo. The source↔release relationship isn't guessable from folder structure (no general rule "folder X releases folder Y"), so the caller passes pairs explicitly.test/release-drift.mjs— temporary repos with explicitGIT_AUTHOR_DATE/GIT_COMMITTER_DATEper commit (not relying on real wall-clock gaps between commits made milliseconds apart in a test run, whichgit log --since's second-level granularity could otherwise make flaky), plus one live check against the real NyxilumLang→NyxilumNode pair that only asserts it doesn't throw.
Docker
docker build -t workspace-status-mcp .
docker run -i --rm -e GH_TOKEN=<token> -v "$HOME/Projects:/projects" workspace-status-mcpThe server speaks MCP over stdio, so keep -i. Needs: your projects folder mounted (pass /projects as projectsRoot) and GH_TOKEN for PR/release checks.
In an MCP client config use "command": "docker" with the same arguments.
Startup timeout
The server itself starts in about a second, but a client launching several MCP servers at once on a cold machine (right after boot) can hit Claude Code's default 30-second startup limit and mark it "failed". That limit belongs to the client, not to the server, so raise it there - in ~/.claude/settings.json:
{ "env": { "MCP_TIMEOUT": "120000" } }(or start Claude Code as MCP_TIMEOUT=120000 claude). Takes effect in the next session.
License
MIT — Faneraiy14.
Available Tools
4 toolscheck_docsДетектор застарілої/відсутньої архітектурної документаціїA
Перевіряє актуальність ~/Projects/Architecture/.txt кожного git-репозиторія: "missing" (документації нема взагалі), "stale" (репо мав нові зміни після документа), "current" (актуально), "no-commits" (репо ще без жодного коміту). Для документів, записаних через write_doc, рахує ТОЧНУ кількість комітів з моменту запису (trackingMethod: "commit"); для решти - грубший запасний варіант за mtime файлу (trackingMethod: "mtime"). НЕ генерує/переписує документацію сам - лише каже, куди дивитись, щоб AI-асистент (чи людина) писав/оновлював цілеспрямовано, а не перечитував усе підряд щосесії. Документація розкидана між кількома незалежними парами корінь+Architecture-тека (напр. репо, винесене з ~/Projects, з документом прямо поруч на новому місці)? Передай points замість projectsRoot/docsRoot.
Inputs: repos, points, docsRoot, projectsRoot, +1 more.
check_release_driftДетектор "реліз відстав від джерела"A
Для явних пар (репо з вихідним кодом, репо, що з нього тегує релізи) рахує, скільки комітів з'явилось у джерелі відколи реліз востаннє тегувався, і наскільки давно найстарший із них. Зв'язок джерело->реліз не вгадується автоматично зі структури тек - передавай пари явно (напр. {source:"NyxilumLang", release:"NyxilumNode"}).
Inputs: pairs (required), projectsRoot (required), only_attention.
sweep_statusЗнімок стану всіх git-репозиторіїв у теціA
Сканує всі git-репозиторії під заданим коренем (типово ~/Projects) і одним викликом повертає, які з них "потребують уваги": незакомічені зміни, неопубліковані коміти, або невдалий/ще не завершений останній CI-запуск. Заміняє ручний цикл git status + gh run list по кожному репо окремо. Репо розкидані між кількома коренями (напр. частина в ~/Projects, частина деінде)? Передай roots замість root - скановуються всі разом, одним викликом.
Inputs: root, repos, roots, check_ci, +1 more.
write_docЗаписати архітектурну документацію репо з прив'язкою до комітуA
Записує ~/Projects/Architecture/.txt і поруч фіксує commit-хеш репозиторія на момент запису. Після цього check_docs може рахувати РЕАЛЬНУ кількість комітів з моменту запису (git rev-list --count) замість грубого порівняння за mtime файлу. Текст документації інструмент не генерує - лише зберігає вже готовий текст (розуміння коду для документування лишається завданням AI/людини).
Inputs: repo (required), content (required), projectsRoot (required), docsRoot.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 4 tools
Each tool targets a distinct concern: sweep_status covers git/CI status, check_docs covers architecture-doc freshness, check_release_drift covers release lag, and write_doc supports doc tracking. Any conceptual overlap, such as staleness, is clearly separated by the resource being checked.
All tool names follow a consistent snake_case verb_noun pattern: sweep_status, check_docs, check_release_drift, write_doc. The check_ prefix is used uniformly for health/diagnostic operations.
Four tools is well-scoped for a workspace-status server: one general status sweep, two specialized checks, and one supporting write operation. No tool is redundant or extraneous.
The set covers the full workflow for its domain: identifying repo status, checking and writing architecture docs, and measuring release drift. The release-drift pair requirement is an explicit design boundary rather than a missing operation.
Related MCP Connectors
Living docs and MCP context for GitHub repos — conventions, gaps, and source-cited pages on merge.
GitHub Copilot MCP: code, repo, PR, workflow tools for AI assistants.
Render, verify, describe, and safely edit Mermaid diagrams through MCP.
Turn a GitHub repo or docs site into agent-ready context: pack it or search it, over MCP.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceProvides MCP tools to scan and navigate markdown repositories, offering graph-based overview, document reading, context retrieval, and orphan detection.MIT
- AlicenseNot gradedqualityCmaintenanceProvides MCP tools for fetching GitHub PR diffs, searching codebases, running unit tests, and linting code, enabling automated code review and CI/CD workflows.MIT
- FlicenseNot gradedqualityCmaintenanceProvides local MCP tools for searching, filtering, adding, updating, and analyzing a catalog of study projects, with statistics and GitHub publication readiness checks, all backed by SQLite.-
- AlicenseNot gradedqualityCmaintenanceProvides MCP tools to manage code review tasks, track evidence and GitHub checks, and record approval decisions in a local kanban board.1MIT