ws-mcp
# ws-mcp
An [MCP](https://modelcontextprotocol.io/) server that gives LLMs full visibility into your [ws-cli](https://github.com/camggould/ws-cli) workspace tree.
Ask your AI assistant things like:
- *"What projects have I abandoned? Summarize where I left off on each one."*
- *"Which workspaces have uncommitted git changes?"*
- *"What tasks are open across all my ws/* projects?"*
- *"Find everything I was working on related to authentication."*
The server traverses your workspace tree, reads metadata, checks git status, parses saved browser tabs, reads [beads](https://beads.dev) task databases, and exposes it all through MCP tools — no state of its own.
## Prerequisites
- **Node.js** >= 18
- **[ws-cli](https://github.com/camggould/ws-cli)** installed and configured (provides the workspace tree that this server reads)
## Installation
```bash
git clone https://github.com/camggould/ws-mcp.git
cd ws-mcp
npm install
```
## Registering with an MCP Client
### Claude Code (per-project)
Add a `.mcp.json` file to any project directory:
```json
{
"mcpServers": {
"ws-mcp": {
"command": "node",
"args": ["/absolute/path/to/ws-mcp/src/server.js"]
}
}
}
```
### Claude Code (global)
```bash
claude mcp add --global ws-mcp node /absolute/path/to/ws-mcp/src/server.js
```
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
```json
{
"mcpServers": {
"ws-mcp": {
"command": "node",
"args": ["/absolute/path/to/ws-mcp/src/server.js"]
}
}
}
```
### Any MCP-compatible client
ws-mcp uses **stdio transport** — pipe stdin/stdout to `node src/server.js`.
## Tools
### `list_workspaces`
List all workspaces with metadata. Filter by status, staleness, or tags.
| Parameter | Type | Description |
|---|---|---|
| `status` | `string` | Filter by status: `active`, `paused`, `archived`, `abandoned` |
| `stale_days` | `number` | Only show workspaces not opened in this many days |
| `tags` | `string` | Comma-separated tags to filter by (matches any) |
```
list_workspaces({})
list_workspaces({ status: "active" })
list_workspaces({ stale_days: 30, tags: "coding,research" })
```
**Returns:** Array of `{ name, status, last_opened, created, tags, path }`.
---
### `get_workspace`
Deep-dive into a single workspace. Returns everything the server knows.
| Parameter | Type | Description |
|---|---|---|
| `name` | `string` | Workspace name (e.g. `"my-project"` or `"parent/child"`) |
```
get_workspace({ name: "ws/cli" })
```
**Returns:**
- **meta** — status, tags, created/last opened dates
- **git** — branch, dirty/clean, ahead/behind remote, modified/untracked/staged file counts
- **tabs** — saved browser tabs from last session (count + URLs)
- **tab_history** — session count, first/last session timestamps, unique URL count across all sessions
- **beads** — task tracker summary: `total_issues`, `by_status` counts, `open_by_priority` counts (or `null` if no beads database)
- **days_since_opened** / **stale** — staleness indicator (stale = 30+ days)
- **notes** — full `workspace.md` content
---
### `get_workspace_tree`
Full parent-child hierarchy as a nested JSON tree. No parameters.
```
get_workspace_tree({})
```
**Returns:** Nested tree with `{ name, children[], meta?, path? }` at each node.
---
### `find_stale_workspaces`
Identify workspaces you may have forgotten about, sorted by most stale first.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `days` | `number` | `14` | Days without activity to consider stale |
```
find_stale_workspaces({ days: 7 })
```
**Returns:** Array of `{ name, status, last_opened, days_since, tags }`.
---
### `search_workspaces`
Full-text search across workspace names, tags, `workspace.md` notes, and saved tab URLs.
| Parameter | Type | Description |
|---|---|---|
| `query` | `string` | Search query (case-insensitive substring match) |
```
search_workspaces({ query: "authentication" })
```
**Returns:** Array of `{ workspace, status, matches[] }` where each match includes the field name and matching value/context.
---
### `summarize_all`
High-level dashboard across all workspaces. No parameters.
```
summarize_all({})
```
**Returns:**
- **total_workspaces** — count
- **by_status** — breakdown (`{ active: 3, paused: 1, ... }`)
- **recently_active** — workspaces opened in the last 7 days
- **stale_30_plus_days** — workspaces untouched for 30+ days
---
### `list_beads`
List individual tasks from a workspace's [beads](https://beads.dev) issue tracker. Supports filtering and returns tasks sorted by priority then creation date.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `workspace` | `string` | *required* | Workspace name (e.g. `"betterlife"` or `"ws/mcp"`) |
| `status` | `string` | | Filter: `open`, `in_progress`, `blocked`, `closed` |
| `priority` | `number` | | Filter: `0`=critical, `1`=high, `2`=normal, `3`=low, `4`=trivial |
| `type` | `string` | | Filter: `bug`, `feature`, `task`, `epic`, `chore` |
| `label` | `string` | | Filter by label (exact match) |
| `limit` | `number` | `50` | Max issues to return (1–200) |
```
list_beads({ workspace: "betterlife" })
list_beads({ workspace: "betterlife", status: "open", priority: 1 })
```
**Returns:** `{ workspace, count, issues[] }` where each issue includes `id`, `title`, `description`, `status`, `priority`, `priority_label`, `type`, `assignee`, `labels[]`, `created_at`, `updated_at`.
---
### `get_beads_across_workspaces`
Aggregate task counts across multiple workspaces. Scope to a parent prefix or query everything.
| Parameter | Type | Description |
|---|---|---|
| `parent` | `string` | Only include workspaces under this prefix (e.g. `"ws"` for ws/* workspaces). Omit for all. |
| `status` | `string` | Only show workspaces that have at least one issue with this status |
```
get_beads_across_workspaces({})
get_beads_across_workspaces({ parent: "ws" })
get_beads_across_workspaces({ status: "open" })
```
**Returns:**
- **aggregate** — `total_open`, `workspaces_with_beads`, `by_status` counts, `open_by_priority` counts
- **workspaces[]** — per-workspace breakdown with `by_status`, `open_by_priority`, `open_total` (sorted by most open issues first)
## How It Works
```
┌──────────────┐ stdio ┌──────────┐ reads ┌──────────────────┐
│ MCP Client │ ◄────────────► │ ws-mcp │ ──────────────► │ ~/Workspaces/ │
│ (Claude, │ JSON-RPC │ server │ │ .workspace.yaml │
│ Cursor, │ │ │ │ tabs.json │
│ etc.) │ │ │ │ tabs-history.jsonl│
└──────────────┘ └──────────┘ │ workspace.md │
│ │ .beads/beads.db │
│ .git/ │
▼ └──────────────────┘
~/.config/ws/
config.json
```
1. Reads `~/.config/ws/config.json` to find the workspaces root (defaults to `~/Workspaces`)
2. Recursively walks the directory tree, following symlinks, looking for `.workspace.yaml` marker files
3. Parses workspace metadata (YAML), saved tabs (`tabs.json`), tab history (`tabs-history.jsonl`), and notes (`workspace.md`)
4. Reads beads SQLite databases (`.beads/beads.db`) in read-only mode for task counts and issue listings
5. Runs `git status` on workspaces that are git repos (via [simple-git](https://github.com/steveukx/git-js))
6. Exposes everything through MCP tools over stdio transport
The server is stateless — it reads directly from the filesystem that ws-cli manages. No database, no cache, no background processes.
## Development
```bash
# Run with auto-reload on file changes
npm run dev
# Run directly
npm start
```
## Dependencies
| Package | Purpose |
|---|---|
| [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) | MCP server framework |
| [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) | Read-only access to beads task databases |
| [simple-git](https://github.com/steveukx/git-js) | Git status checks |
| [js-yaml](https://github.com/nodeca/js-yaml) | Workspace metadata parsing |
| [fast-glob](https://github.com/mrmlnc/fast-glob) | File pattern matching |
| [zod](https://github.com/colinhacks/zod) | Tool parameter validation |
## Related
- **[ws-cli](https://github.com/camggould/ws-cli)** — the CLI tool that creates and manages the workspace tree this server reads
## License
MIT
TDQS
Scored across 8 tools
Most tools have clear, distinct purposes, but list_workspaces (with staleness filter) overlaps with find_stale_workspaces, and summarize_all also reports stale counts, causing potential confusion. The descriptions help differentiate, but an agent might misselect between these related tools.
All names are lowercase with underscores, but verbs vary (list, get, find, summarize, search). The verbs are semantically appropriate for each action, so the pattern is intuitive, though not as uniform as a pure verb_noun convention.
8 tools is well within the ideal 3-15 range for a workspace management server. Each tool covers a distinct aspect of workspace queries and bead aggregation without unnecessary bloat.
The tool set provides thorough read/query coverage for workspaces and bead tasks, including listing, details, hierarchy, staleness, search, and summaries. However, it lacks any mutation tools (create/update/delete) for workspaces or tasks, which may be a minor gap depending on the intended read-only scope.