Skip to main content
Glama
Faneraiy14
by Faneraiy14
README.md
# workspace-status-mcp

*[Українською](README.uk.md)*

An MCP server with five 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
  looping `git status` + `gh run list` over dozens of repos one at a time.
- `check_docs` — flags which repos' `Architecture/<repo>.txt` doc is
  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 `Architecture/<repo>.txt` and stamps it with the
  repo's current commit hash, so `check_docs` can 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.
- `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 looping `gh pr view` + two separate
  `gh api .../comments` calls 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.

## 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 the projects folder never had an `Architecture/` folder
at all — 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):

```json
{
  "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 instead of in the central `Architecture/` folder. 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:

1. CLI args (`args[2]`/`args[3]` after the event name) — a one-off
   single-point override.
2. **The watch-points config file** — `~/.claude/workspace-status-points.json`
   by default (override the path with `WATCH_POINTS_FILE`). This is the
   normal way to manage this day to day:
   ```json
   {
     "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 `points` array, falls through to the
   next source below.
3. `PROJECTS_ROOT`/`DOCS_ROOT` environment variables — single-point
   fallback for anyone registering the hook through a shell command
   instead of the `args` exec form.
4. 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

```bash
npm install
```

Requires 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 five 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:

```bash
git pull && npm install
```

Take 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 |
|---|---|---|---|
| `root` | string | — (required unless `roots`) | Folder to scan, one level deep (e.g. `/home/user/Projects`) |
| `roots` | string[] | — | Several roots in one call instead of one `root` — results are merged (e.g. repos split between `~/Projects` and elsewhere) |
| `repos` | string[] | all subfolders | Limit to specific repo names instead of scanning everything (matched across all roots) |
| `check_ci` | boolean | `true` | Also query GitHub Actions for each repo's latest run |
| `only_attention` | boolean | `true` | Only return repos that need a look; `false` returns everything |

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 |
|---|---|---|---|
| `projectsRoot` | string | — (required unless `points`) | Folder with the repos |
| `docsRoot` | string | `<projectsRoot>/Architecture` | Folder with the `<repo>.txt` docs |
| `points` | `{projectsRoot, docsRoot?}[]` | — | Several independent projectsRoot+docsRoot pairs in one call instead of one `projectsRoot`/`docsRoot` (e.g. a repo moved out of `~/Projects`, doc sitting right next to it wherever it went) |
| `repos` | string[] | all subfolders | Limit to specific repo names (applied within each point independently) |
| `only_attention` | boolean | `true` | Only return missing/stale; `false` returns everything including `current` |

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 |
|---|---|---|---|
| `projectsRoot` | string | — (required) | Folder with the repos |
| `repo` | string | — (required) | Repo folder name (e.g. `"anylint"`) |
| `content` | string | — (required) | Full text to write to `<repo>.txt` |
| `docsRoot` | string | `<projectsRoot>/Architecture` | Folder with the `<repo>.txt` docs |

Writes `<repo>.txt` and, next to it, `.meta/<repo>.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.

## Tool: `check_release_drift`

| Argument | Type | Default | Meaning |
|---|---|---|---|
| `projectsRoot` | string | — (required) | Folder with the repos |
| `pairs` | `{source, release}[]` | — (required) | Explicit list of source→release folder-name pairs |
| `only_attention` | boolean | `true` | Only return `drifted`; `false` returns everything including `current`/`no-tags` |

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 |
|---|---|---|---|
| `prs` | `{repo, number}[]` | — (required) | PRs to check, `repo` as `"owner/name"` |
| `only_attention` | boolean | `true` | Only return PRs that are `DIRTY` (merge conflict), `CHANGES_REQUESTED`, or have a failing CI check; `false` returns everything |

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 `.git` folders one level under
  a root (exported as `findGitRepos`, reused by `docs.js`), then for each
  one runs `git branch`/`git status`/`git rev-list` and (optionally)
  `gh run list` in parallel, batched at 8 repos at a time to avoid
  hammering the GitHub API. `sweepStatus()`'s `roots` (plural) runs
  `findGitRepos` per root and merges the results before filtering by
  `repos`/`only_attention` — so a repo name filter matches regardless of
  which root it actually lives under.
- `src/docs.js` — `checkDocs()`: prefers commit-based tracking
  (`.meta/<repo>.json`, written by `write_doc`) when available; falls
  back to comparing `git log -1 --format=%ct` against the doc file's
  mtime for docs written directly. A repo with no commits yet reports
  `no-commits` rather than being silently lumped into `missing` or
  `current`. `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 `<repo>.txt` plus
  `.meta/<repo>.json` (`{commitHash, writtenAt}`, `HEAD` at 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()`: fetches `gh pr view` plus both
  comment endpoints (`issues/{n}/comments` for top-level,
  `pulls/{n}/comments` for 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 (decides `needsAttention`,
  picks the latest comment of each kind) — no network, so it's tested
  with fixtures independently of the real `gh` calls.
- `src/server.js` — registers all five tools with the MCP SDK over the
  stdio transport.
- `test/smoke.mjs` — `sweep_status` against 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 `~/Projects` used
  to serve this, until it got renamed away mid-session and silently broke
  the test — not something to depend on again), plus a `roots` (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 leftover `DIRTY` state),
  plus `checkPrStatus()` against real, permanently-merged PRs in two
  different repos (`mergedAt`/`state` won'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_docs` against temporary, throwaway git repos
  with controlled commit/file timestamps (real `~/Projects` drifts over
  time, which would make a fixed test flaky), including both tracking
  methods and a `points` (plural) case.
- `test/write-doc.mjs` — `writeDoc()` against a temporary git repo:
  correct `HEAD` captured, `.meta/` created on demand, empty content
  rejected.
- `test/hook.mjs` — `hooks/check-docs-reminder.mjs` as a real subprocess
  (it's a CLI entry point, not a library function): silent outside any
  watch point, silent with no `Architecture/` folder at all, reminds and
  resolves the right repo from a nested subdirectory, the watch-points
  config file driving two independent points at once, and an empty/absent
  `points` array in that file falling through to the next source.
- `src/release-drift.js` — `checkReleaseDrift()`: finds the release
  repo's most recent tag via `git for-each-ref --sort=-creatordate`
  (sorted by actual tag time, not the semver-string sort `-v:refname`
  would give — `v1.10` would otherwise sort before `v1.9`), then counts
  `git 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 explicit
  `GIT_AUTHOR_DATE`/`GIT_COMMITTER_DATE` per commit (not relying on real
  wall-clock gaps between commits made milliseconds apart in a test run,
  which `git 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.

## License

MIT — Faneraiy14.

TDQS

A4.4/5.0

Scored across 4 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness5/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues