Files
ops-bridge/scripts/register_mcp.py
tegwick 365c0d611a feat(BRIDGE-WP-0003): MCP server, /bridge-status skill, cross-mode coverage enforcement
Implements the full BRIDGE-WP-0003 workplan: 188 tests passing, 0 lint errors.

## What's added

**Capability registry** (`src/bridge/capabilities.py`):
- 10 capabilities with required_access_modes (cli/mcp/skill)
- Single source of truth for what OpsBridge does and where

**MCP server** (`src/bridge/mcp_server/server.py`):
- 10 FastMCP tools: bridge_up/down/restart/status/logs + 5 catalog_* tools
- 3 resources: bridge://status, catalog://domains, catalog://targets
- `.mcp.json` for project-scope auto-registration
- `scripts/register_mcp.py` for user-scope machine-global registration

**Skill** (`~/.claude/plugins/ops-bridge/bridge-status.md`):
- /bridge-status: health table with emoji indicators + remediation advice

**Cross-mode test coverage enforcement**:
- `tests/conftest.py`: capability/access_mode marks + collect_capability_coverage()
- `tests/test_mcp.py`: 31 FastMCP in-process client tests (Client(mcp) pattern)
- `tests/test_skill.py`: static skill lint against capability registry
- `tests/test_coverage_completeness.py`: meta-test that fails if any required
  (capability × mode) pair lacks a test; also validates CLI commands and MCP
  tools are registered in the capability registry

**ADR** (`architecture/adr-001-cross-mode-capability-registry.md`):
- Documents the registry pattern and FastMCP 3.x testing approach

Key implementation note: FastMCP 3.x in-process results are in
result.content[0].text (JSON string), not result.data directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 11:33:16 +01:00

97 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Register the ops-bridge MCP server at user scope in ~/.claude.json.
Usage:
python scripts/register_mcp.py [--dry-run]
This script:
1. Reads the MCP server config from .mcp.json in the repo root.
2. Calls `claude mcp add-json -s user ops-bridge <config>` to register.
3. Patches the `cwd` field in ~/.claude.json (claude mcp add-json silently drops it).
After running, all Claude Code sessions on this machine have access to the
`ops-bridge` MCP tools — even when opened outside the ops-bridge repo directory.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).parent.parent
MCP_JSON = REPO_ROOT / ".mcp.json"
CLAUDE_JSON = Path.home() / ".claude.json"
SERVER_NAME = "ops-bridge"
def load_server_config() -> dict:
data = json.loads(MCP_JSON.read_text())
servers = data.get("mcpServers", {})
if SERVER_NAME not in servers:
raise SystemExit(f"ERROR: '{SERVER_NAME}' not found in {MCP_JSON}")
return servers[SERVER_NAME]
def register(config: dict, dry_run: bool) -> None:
config_json = json.dumps(config)
cmd = ["claude", "mcp", "add-json", "-s", "user", SERVER_NAME, config_json]
print(f"→ Running: {' '.join(cmd[:6])} '<config>'")
if not dry_run:
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"FAILED:\n{result.stderr}", file=sys.stderr)
raise SystemExit(1)
print(f" OK: {result.stdout.strip()}")
def patch_cwd(cwd: str, dry_run: bool) -> None:
"""Patch the cwd field that claude mcp add-json silently drops."""
if not CLAUDE_JSON.exists():
print(f"WARNING: {CLAUDE_JSON} not found — skipping cwd patch")
return
data = json.loads(CLAUDE_JSON.read_text())
servers = data.setdefault("mcpServers", {})
if SERVER_NAME not in servers:
print(f"WARNING: '{SERVER_NAME}' not found in {CLAUDE_JSON} after registration")
return
current_cwd = servers[SERVER_NAME].get("cwd")
if current_cwd == cwd:
print(f"→ cwd already correct: {cwd}")
return
servers[SERVER_NAME]["cwd"] = cwd
print(f"→ Patching cwd: {cwd}")
if not dry_run:
CLAUDE_JSON.write_text(json.dumps(data, indent=2) + "\n")
print(" OK")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--dry-run", action="store_true", help="Show what would be done without making changes")
args = parser.parse_args()
if args.dry_run:
print("[DRY RUN] No changes will be made.\n")
config = load_server_config()
cwd = config.get("cwd", str(REPO_ROOT))
print(f"Registering ops-bridge MCP server from {MCP_JSON}")
register(config, dry_run=args.dry_run)
patch_cwd(cwd, dry_run=args.dry_run)
if not args.dry_run:
print("\nDone. Restart Claude Code for the changes to take effect.")
else:
print("\n[DRY RUN complete]")
if __name__ == "__main__":
main()