generated from coulomb/repo-seed
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>
97 lines
3.1 KiB
Python
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()
|