tcm-mcp
# tcm-mcp — TCM MCP Server (Epic 1: Test Case CRUD)
A stdio [MCP](https://modelcontextprotocol.io) server that gives AI agents (Torque, triage-e2e, Claude agents) a stable tool interface to read and write **TCM** test cases — without touching the database schema directly.
It is a **thin client**: every tool call proxies a TCM REST endpoint. ID resolution, validation, `display_id` generation, the `in_cicd` lock, and soft-delete scoping all happen inside TCM. Agents reference cases by **`display_id`** (e.g. `APA-3`); internal UUIDs are never exposed.
Full design: [`docs/features/mcp-e1-test-case-crud.md`](https://github.com/JoinFullStackDev/TCM/blob/main/docs/features/mcp-e1-test-case-crud.md) (in the main TCM repo).
## Tools
| Tool | Purpose |
| ------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `list_projects` | Discover the projects you can see (`project_id` + `name`). Search by name; default 50, max 200. |
| `search_suite` | Resolve a suite name/prefix → `suite_id` within a project (`project_id` **or** `project_name`). |
| `list_suites` | List **every** suite in a project — each with `suite_id`, `name`, `prefix`, `group` (role label), `test_case_count`. |
| `create_suite` | Create a suite in a project and return its `suite_id`. Commits immediately — **no dry-run** (see below). |
| `list_test_cases` | Lightweight filterable list (`display_id`, `title`, `automation_status`, `priority`); filter by `tags`. Default 50, max 200. |
| `get_test_case` | Full detail + steps, by `display_id`. |
| `create_test_case` | Create a case with steps — **dry-run → approval → commit** (see below). |
| `update_test_case` | Partial update; steps are **full-replace** when provided — same dry-run flow. |
Reads exclude trashed (soft-deleted) cases. Test-case writes (`create_test_case`, `update_test_case`) require the dry-run flow. `create_suite` is the one exception — it commits on the first call, because a suite is an empty container with nothing to diff and no destructive edit to preview.
**Project scoping.** `search_suite`, `list_suites`, and `list_test_cases` scope by project. Pass a `project_id` (UUID) directly, or a `project_name` — the server resolves the name to an id via `list_projects` (case-insensitive exact match; an unknown name returns `NOT_FOUND` and an ambiguous one returns `AMBIGUOUS` with the candidate ids). Use `list_projects` first to discover ids. `list_test_cases` with no project returns cases across every project you can see.
**Tag filtering.** `list_test_cases` takes a `tags` array — `{ "tags": ["needs-qa-review", "imported-from-dev-tests"] }` returns cases carrying **any** of them (case-insensitive), each with its full `tags` list. TCM filters in the database across the whole project, so `total` is the real match count. Tags are stored canonically (trimmed, lowercased, de-duplicated), so `Smoke` and `smoke` are one tag.
## Requirements
- **Node.js ≥ 18** (for `npx` and the global `fetch`).
- **Git read access** to `JoinFullStackDev/tcm-mcp` — the package is distributed by **git URL, not published to npm**. On headless hosts (OpenClaw/Torque) a git token must be present in the environment.
- That's it for the TCM URL: it **defaults to production** (`https://tcm-ochre.vercel.app`), so there's nothing to look up or set. You just need to authenticate ([Quickstart](#quickstart-2-steps)).
Because it's distributed by git URL, `npx` clones the repo and **builds from source on first run** (via the package's `prepare` → `tsc` step), so the first launch is slower. Subsequent runs are cached.
## Quickstart
Zero-config: the production TCM instance (`https://tcm-ochre.vercel.app`) is **baked in as the default**, so you never set `TCM_BASE_URL`. Point at a different instance only if you self-host (see [Environment variables](#environment-variables)).
**Prerequisite — git access.** This package is fetched by git URL from a **private** repo, so the machine running it needs git read access (`gh auth login`, or a git token for headless hosts). Node ≥ 18 must be installed. On macOS, GUI-launched Claude Desktop may not see your shell `PATH` — if the server fails to start, use an absolute path to `npx` in the config (find it with `which npx`).
### 1. Register the server
**Claude Code** — one command, nothing to edit by hand:
```bash
claude mcp add tcm --scope user -- npx --yes github:JoinFullStackDev/tcm-mcp#v1.7.0 --stdio
```
`--scope user` makes it available in every project. (Drop it to scope to the current project; Claude Code writes the `.mcp.json` for you.)
**Claude Desktop** — no CLI, so add it to the config file once:
1. **Settings → Developer → Edit Config** — this creates and opens `claude_desktop_config.json` for you (no folder to make yourself):
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
2. Add the `tcm` entry (merge into `mcpServers` if it already exists), then fully **quit + reopen** Claude Desktop:
```json
{
"mcpServers": {
"tcm": {
"command": "npx",
"args": ["--yes", "github:JoinFullStackDev/tcm-mcp#v1.7.0", "--stdio"]
}
}
}
```
> **Pin to a tag** (`#v1.7.0`), **not a branch** — a branch ref re-resolves on every launch and can trip the 30 s MCP startup timeout. No `env` block is needed.
### 2. Sign in
The server starts even before you've logged in — it just exposes a **`login` tool**. So the easiest way (works in Claude Desktop **and** Claude Code, no terminal):
> **Just ask Claude:** _"Log me into TCM."_
Claude calls the `login` tool, a browser opens once for Google sign-in, and the server stores a session it then keeps refreshed. The other tools light up immediately after. (Playwright is auto-installed on first login — a one-time ~100 MB browser download into `~/.tcm-mcp`.)
Prefer a terminal? Same thing, run once:
```bash
npx --yes github:JoinFullStackDev/tcm-mcp#v1.7.0 login
```
Details: [Auto-refreshing login](#auto-refreshing-login-recommended).
<details>
<summary>Manual <code>.mcp.json</code> / legacy static-token setup</summary>
If you'd rather edit `.mcp.json` directly (Claude Code project or `~/.claude/.mcp.json`), the minimal entry is just `command` + `args` as shown above. To use the **legacy static-token** mode instead of a login session (e.g. CI that already has a JWT), add an env block — note it **expires ~1h** and refreshing it needs a full client restart:
```jsonc
{
"mcpServers": {
"tcm": {
"command": "npx",
"args": ["--yes", "github:JoinFullStackDev/tcm-mcp#v1.7.0", "--stdio"],
"env": { "TCM_USER_TOKEN": "${TCM_USER_TOKEN}" },
},
},
}
```
</details>
## Auth modes
The server resolves its mode at startup. **Precedence: `CLUTCH_API_KEY` → login session file → `TCM_USER_TOKEN`.**
| Mode | Selected by | Sends | Use for | Attribution |
| ---------------------------------- | -------------------------------- | ----------------------------- | ------------------------------ | --------------------------------------------- |
| **Refreshing token** (interactive) | a session file (`npm run login`) | `Authorization: Bearer <jwt>` | Claude Code, human in the loop | The real user (their Supabase session) |
| **Static token** (legacy) | `TCM_USER_TOKEN` | `Authorization: Bearer <jwt>` | CI / scripts injecting a JWT | The real user (their Supabase JWT) |
| **Clutch key** (headless) | `CLUTCH_API_KEY` | `X-Clutch-Key` | Torque via Clutch/OpenClaw | The service profile — see `MCP_AGENT_USER_ID` |
In **refreshing** mode the server auto-renews the access token before expiry and again on any `401` (retrying the request once), and persists the rotated refresh token back to the session file. In **static** and **clutch** modes a `401` is terminal (nothing to refresh).
In **headless** mode you **must** also set `MCP_AGENT_USER_ID`, or `create`/`update` will fail on the `created_by`/`updated_by` NOT NULL constraint. The server prints a startup warning if it's missing. When set, the server forwards it to TCM as an `X-Agent-User-Id` header (trusted only alongside a valid `X-Clutch-Key`), so each agent attributes its own writes; TCM falls back to its own `MCP_AGENT_USER_ID` env if the header is absent. (Requires TCM with the matching write-attribution support.)
## Auto-refreshing login (recommended)
The login helper signs you into TCM in a browser once and writes a **session file** the server then uses to keep itself authenticated indefinitely — no `~1h` token churn, no client restarts.
```bash
# no clone needed — runs straight from the git URL:
npx --yes github:JoinFullStackDev/tcm-mcp#v1.7.0 login
# ...or, from a local clone of this repo:
npm run login
```
- Opens a browser **only** if there's no valid saved session; later runs refresh silently (headless, no window).
- **Playwright is installed for you on first login.** It is deliberately _not_ a server dependency (keeps `npx <server>` installs lean ~50 MB), so the login helper installs `playwright` + Chromium once into `~/.tcm-mcp` (a ~100 MB one-time download) if they aren't already present. You do **not** need a separate "Playwright MCP" — the login is fully self-contained.
It writes `~/.tcm-mcp/session.json` (mode `0600`) containing the Supabase project URL, anon key (public), and the access + **refresh** tokens. From then on the MCP server (mode “refreshing token”) mints fresh access tokens on demand.
- **Session file location:** `~/.tcm-mcp/session.json`, override with `TCM_SESSION_FILE`.
- **Browser profile:** `~/.tcm-mcp/browser`, override with `TCM_BROWSER_PROFILE`.
- **Security:** the refresh token is a long-lived credential — the file is `0600` and must never be committed or shared. Supabase rotates the refresh token on every refresh; the server persists the new one atomically.
- **When it expires:** if the refresh token is ever revoked/expired, tool calls fail with a clear “run `npm run login`” message. Re-run the helper.
- **Anon key capture:** the helper sniffs the public `apikey` header from Supabase network traffic. If capture ever fails, set `SUPABASE_ANON_KEY` (safe to expose) and re-run.
## Environment variables
| Variable | Required | Mode | Purpose |
| --------------------- | -------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `TCM_BASE_URL` | no (defaults to production) | all | Base URL of the TCM instance. Defaults to `https://tcm-ochre.vercel.app`; set only to point at a preview / self-hosted instance. |
| `TCM_SESSION_FILE` | no | refreshing | Override the session-file path (default `~/.tcm-mcp/session.json`). |
| `TCM_BROWSER_PROFILE` | no | refreshing | Override the login browser-profile dir (default `~/.tcm-mcp/browser`). |
| `TCM_USER_TOKEN` | one credential | static | User's Supabase JWT (legacy; expires ~1h, no refresh). |
| `CLUTCH_API_KEY` | one credential | headless | Server-to-server key; must match TCM's `CLUTCH_API_KEY`. |
| `MCP_AGENT_USER_ID` | yes, in headless mode for writes | headless | `profiles.id` UUID of the Clutch Agent service profile, for write attribution. |
The recommended credential is the **login session file** (`npm run login`), not `TCM_USER_TOKEN` — see [Auto-refreshing login](#auto-refreshing-login-recommended). `TCM_USER_TOKEN` remains for CI / scripts that already have a JWT.
## The write safety flow (dry-run → approval → commit)
`create_test_case` and `update_test_case` are two-pass:
1. Call with **`dry_run: true`** first. The tool validates, resolves IDs, and **returns a summary** (create: the proposed case; update: a field-level diff + before/after steps). **No write happens.**
2. A human reviews and approves — Torque relays the summary to Slack via Clutch; Claude Code shows it inline in the chat.
3. Call again with **`dry_run: false`** (or omit `dry_run`) to commit.
The server does **not** technically enforce that a dry-run/approval happened before a commit (decided: PRD OQ-4 Option A) — it's a process convention. Don't call with `dry_run: false` without human approval.
## Local development
```bash
git clone https://github.com/JoinFullStackDev/tcm-mcp && cd tcm-mcp
npm install # runs prepare → tsc → dist/
npm run build # rebuild after changes
# run the stdio server directly (Ctrl-D / EOF to exit)
TCM_BASE_URL=https://your-tcm-instance.example.com \
TCM_USER_TOKEN=your-jwt \
node dist/index.js
npm run dev # same, via ts-node (no build step)
```
Startup logs (mode, base URL, "Ready") are written to **stderr**, so they don't interfere with the stdio MCP protocol on stdout.
## Notes & caveats
- **Audit logging** (`mcp_tool_calls`, PRD Appendix C) requires migration `00042` applied to the TCM database. The log inserts are **fire-and-forget and non-blocking** — if the table is missing, tools still work; only the audit trail is skipped.
- **Distribution** is git-URL only (no npm publish). Pin a tag; ensure hosts have git access.
- The `--stdio` arg in the config is cosmetic — stdio is the only transport.
TDQS
Scored across 5 tools
Each tool has a distinct purpose: create, update, get, list, and suite search. No two tools overlap in function, and search_suite clearly serves as a helper for resolving suite IDs before other operations.
All tools use a verb_noun snake_case pattern (create_, update_, get_, list_, search_). The only deviation is list_test_cases using plural while others use singular, which is a standard convention for list operations.
Five tools is well-scoped for a test case management server. Each tool covers a necessary operation without redundancy or bloat.
Core CRUD operations are present (create, get, update, list), but delete_test_case is missing. Suite management is limited to search, with no create/update/delete for suites, leaving notable lifecycle gaps.