SDLC workflow MCP server for Claude Code agents.
This server is cross-platform — every tool that touches a code host (GitHub or GitLab) dispatches through a single typed platform adapter rather than shelling out to gh / glab inline. Handlers stay platform-agnostic: they resolve the adapter via getAdapter(), call one interface method, and unwrap a three-way AdapterResult<T> (ok: true for success, ok: false for runtime failure, platform_unsupported: true for structural cross-platform asymmetry). The third arm is what replaces the old silent-ignore bug class — e.g. skip_train: true on GitLab used to fake-succeed; it now surfaces as an explicit typed signal.
The canonical exemplars of the pattern live in lib/adapters/pr-merge-github.ts and lib/adapters/pr-merge-gitlab.ts (per R-03 of the retrofit dev spec) — one pair of per-method per-platform files, colocated .test.ts files mocking at the child_process boundary, and an assembler (lib/adapters/github.ts / lib/adapters/gitlab.ts) that wires them into the PlatformAdapter interface declared in lib/adapters/types.ts.
Two CI gate-greps (scripts/ci/gate-greps.sh) enforce the dispatch model: Gate 1 (R-09) forbids platform === 'github' | 'gitlab' branching inside handlers/, and Gate 2 (R-10) forbids direct execSync('gh ...') / execSync('glab ...') / Bun.spawnSync calls from handler files. A runtime contract test (lib/adapters/types.test.ts) additionally asserts that every method in PLATFORM_ADAPTER_METHODS is implemented on both adapters. See docs/adapters/README.md for the full contract, file layout, dispatch model, hybrid sub-call pattern, testing conventions, and the guide for adding a new platform-aware method.
- Bun >= 1.0
- A
GITHUB_TOKENorGITLAB_TOKENenvironment variable (required for tools that interact with GitHub/GitLab APIs) - Python 3.11+ (optional, but required for
commutativity_verify— see commutativity-probe below)
-
Install the binary:
curl -fsSL https://raw.githubusercontent.com/Wave-Engineering/mcp-server-sdlc/main/scripts/install-remote.sh | bashInstall-time options:
Variable / flag Effect SDLC_VERSION=v1.2.3Override sdlc-server release tag (default: latest release) SDLC_PROBE_REF=<git-ref>Override commutativity-probe git ref (default: v0.1.0)--skip-probeSkip commutativity-probe install (handler degrades to verdict: PROBE_UNAVAILABLE) -
Configure your token in
~/.claude.jsonunder thesdlc-serverentry:{ "mcpServers": { "sdlc-server": { "command": "~/.local/bin/sdlc-server", "args": [], "env": { "GITHUB_TOKEN": "<your-token>" } } } } -
Restart Claude Code to activate the server.
Tools are auto-discovered at build time via a glob pattern over handlers/. To add a tool, drop a file in handlers/ that exports a HandlerDef default. No other files need to change.
// handlers/my_tool.ts
import { z } from 'zod';
import type { HandlerDef } from '../types.js';
const handler: HandlerDef = {
name: 'my_tool',
description: 'Does something useful',
inputSchema: z.object({ input: z.string() }),
execute: async (args) => ({
content: [{ type: 'text', text: `Result: ${(args as { input: string }).input}` }],
}),
};
export default handler;The commutativity_verify MCP tool shells out to the
commutativity-probe
Python CLI to compute changeset commutativity from real git diffs. The
installer bundles it via pip install --user (pinned to v0.1.0).
If the probe binary is missing from PATH, commutativity_verify returns
the same body shape as a timeout, with verdict: "PROBE_UNAVAILABLE":
{
"ok": true,
"mode": "pairwise",
"verdict": "PROBE_UNAVAILABLE",
"group_verdict": "PROBE_UNAVAILABLE",
"pairs": [],
"pairwise_results": [],
"warnings": ["commutativity-probe binary not found on PATH; install via mcp-server-sdlc/scripts/install-remote.sh"]
}Callers should treat PROBE_UNAVAILABLE as conservative-fail (sequential
merge fallback) — equivalent to ORACLE_REQUIRED for dispatch purposes.
To install or upgrade the probe manually:
pip install --user 'git+https://github.com/Wave-Engineering/commutativity-probe.git@v0.1.0'Two tools, deliberately split by what the caller cares about:
| Tool | Returns | Use when |
|---|---|---|
pr_merge |
Eager — enrolled:true always; merged reflects the moment-of-call truth (true for direct merge, false for queue path until the queue lands). |
You need the platform to accept the merge, then keep working. Don't care exactly when the commit lands. |
pr_merge_wait |
Blocking — guarantees merged:true, pr_state:"MERGED" on success, or a timeout error. |
You need the commit observable on main before the next step (e.g. git pull, post-merge CI, downstream wave work). |
Both return the same aggregate envelope:
{
"ok": true,
"number": 42,
"enrolled": true,
"merged": false,
"merge_method": "merge_queue",
"queue": { "enabled": true, "position": null, "enforced": true },
"pr_state": "OPEN",
"url": "https://github.com/org/repo/pull/42",
"warnings": []
}When a repo enforces a merge queue via ruleset, GitHub ignores skip_train. Both tools detect this upfront and silently drop the flag, surfacing a warnings[] entry rather than erroring. On non-enforced repos skip_train:true honors the flag (direct merge, no queue fallback) — useful when commutativity_verify has proven the merge safe.
Pre-v1.7.0 callers expected pr_merge to return merged:true after enrolling in the queue (eager-but-misleading). The new aggregate response always reflects the truth: merged:false until the commit is on main. Callers that need the old "wait for it" behavior should switch to pr_merge_wait.
See docs/tool-reference.md (coming soon).