2026-03-12 09:36:19 +01:00
|
|
|
|
---
|
|
|
|
|
|
id: BRIDGE-WP-0003
|
|
|
|
|
|
type: workplan
|
|
|
|
|
|
title: "OpsBridge MCP Server, Skill, and Cross-Mode Test Coverage"
|
2026-06-22 23:16:27 +02:00
|
|
|
|
domain: infotech
|
2026-03-12 09:36:19 +01:00
|
|
|
|
repo: ops-bridge
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
owner: Bernd
|
|
|
|
|
|
topic_slug: custodian
|
|
|
|
|
|
state_hub_workstream_id: 97009d3f-fd92-4fd9-a308-6c2445b4d623
|
|
|
|
|
|
created: "2026-03-12"
|
|
|
|
|
|
updated: "2026-03-12"
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# BRIDGE-WP-0003 — OpsBridge MCP Server, Skill, and Cross-Mode Test Coverage
|
|
|
|
|
|
|
|
|
|
|
|
**Scope:** Expose OpsBridge and OpsCatalog functionality as a FastMCP server
|
|
|
|
|
|
and a Claude Code skill. Introduce a capability registry and cross-access-mode
|
|
|
|
|
|
test suite that enforces test coverage parity across CLI, MCP, and skill for
|
|
|
|
|
|
every operation — including a meta-test that validates the test suite itself is
|
|
|
|
|
|
complete.
|
|
|
|
|
|
|
|
|
|
|
|
**Depends on:** BRIDGE-WP-0001 and BRIDGE-WP-0002 complete.
|
|
|
|
|
|
**Out of scope:** Identity provider integration (FR-27–29, deferred indefinitely).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Goal
|
|
|
|
|
|
|
|
|
|
|
|
After this workplan:
|
|
|
|
|
|
|
|
|
|
|
|
1. Any Claude Code agent can call `bridge_up()`, `bridge_status()`,
|
|
|
|
|
|
`catalog_list_targets()` etc. as first-class MCP tools — no Bash
|
|
|
|
|
|
required, structured JSON in/out.
|
|
|
|
|
|
2. Human operators can invoke `/bridge-status` as a skill to get an
|
|
|
|
|
|
immediate, natural-language summary of tunnel health.
|
|
|
|
|
|
3. Adding any new capability (CLI command, MCP tool) without writing tests
|
|
|
|
|
|
for all required access modes causes `uv run pytest` to fail with a
|
|
|
|
|
|
clear capability × mode gap report.
|
|
|
|
|
|
4. The gap-detection mechanism is itself tested: a synthetic missing-mode
|
|
|
|
|
|
fixture asserts the meta-test catches it.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Reference Documents
|
|
|
|
|
|
|
|
|
|
|
|
| Document | Location |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| Architecture note | `CLAUDE.md` — Architecture section |
|
|
|
|
|
|
| OpsBridge FRS | `wiki/OpsBridgeFrs.md` |
|
|
|
|
|
|
| State Hub MCP server (reference impl) | `~/the-custodian/state-hub/mcp_server/server.py` |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Architecture Summary
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
src/bridge/
|
|
|
|
|
|
capabilities.py # canonical capability registry
|
|
|
|
|
|
mcp_server/
|
|
|
|
|
|
__init__.py
|
|
|
|
|
|
server.py # FastMCP app, stdio entry point
|
|
|
|
|
|
|
|
|
|
|
|
.mcp.json # project-scope MCP registration
|
|
|
|
|
|
scripts/
|
|
|
|
|
|
register_mcp.py # user-scope registration helper
|
|
|
|
|
|
|
|
|
|
|
|
~/.claude/plugins/
|
|
|
|
|
|
ops-bridge/
|
|
|
|
|
|
bridge-status.md # /bridge-status skill
|
|
|
|
|
|
|
|
|
|
|
|
tests/
|
|
|
|
|
|
conftest.py # capability + access_mode marks, collector helper
|
|
|
|
|
|
test_cli.py # existing — annotated with marks (T09)
|
|
|
|
|
|
test_mcp.py # new — FastMCP in-process client tests
|
|
|
|
|
|
test_skill.py # new — static skill coverage lint
|
|
|
|
|
|
test_coverage_completeness.py # new — cross-mode meta-test
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Capability Registry
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
# src/bridge/capabilities.py
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
|
|
ACCESS_MODES = {"cli", "mcp", "skill"}
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class Capability:
|
|
|
|
|
|
name: str
|
|
|
|
|
|
description: str
|
|
|
|
|
|
required_access_modes: frozenset[str]
|
|
|
|
|
|
|
|
|
|
|
|
CAPABILITIES: list[Capability] = [
|
|
|
|
|
|
Capability("bridge_up", "Start one or all tunnels", frozenset({"cli", "mcp"})),
|
|
|
|
|
|
Capability("bridge_down", "Stop one or all tunnels", frozenset({"cli", "mcp"})),
|
|
|
|
|
|
Capability("bridge_restart", "Restart one or all tunnels", frozenset({"cli", "mcp"})),
|
|
|
|
|
|
Capability("bridge_status", "Show tunnel status", frozenset({"cli", "mcp", "skill"})),
|
|
|
|
|
|
Capability("bridge_logs", "Tail tunnel audit log", frozenset({"cli", "mcp"})),
|
|
|
|
|
|
Capability("catalog_list_targets", "List catalog targets", frozenset({"cli", "mcp"})),
|
|
|
|
|
|
Capability("catalog_show_target", "Show target metadata", frozenset({"cli", "mcp"})),
|
|
|
|
|
|
Capability("catalog_list_domains", "List catalog domains", frozenset({"cli", "mcp"})),
|
|
|
|
|
|
Capability("catalog_validate", "Validate catalog consistency", frozenset({"cli", "mcp"})),
|
|
|
|
|
|
Capability("catalog_show_bridge", "Show bridge metadata", frozenset({"cli", "mcp"})),
|
|
|
|
|
|
]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Cross-Mode Test Marks
|
|
|
|
|
|
|
|
|
|
|
|
Every test that exercises a capability against an access mode carries two marks:
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
@pytest.mark.capability("bridge_up")
|
|
|
|
|
|
@pytest.mark.access_mode("cli")
|
|
|
|
|
|
def test_bridge_up_cli(runner, config_file):
|
|
|
|
|
|
result = runner.invoke(app, ["up", "my-tunnel"])
|
|
|
|
|
|
assert result.exit_code == 0
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.capability("bridge_up")
|
|
|
|
|
|
@pytest.mark.access_mode("mcp")
|
|
|
|
|
|
async def test_bridge_up_mcp(mcp_client):
|
|
|
|
|
|
result = await mcp_client.call_tool("bridge_up", {"tunnel": "my-tunnel"})
|
|
|
|
|
|
assert result["started"] == ["my-tunnel"]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Meta-Test Mechanism
|
|
|
|
|
|
|
|
|
|
|
|
`test_coverage_completeness.py` uses a pytest plugin hook to collect all
|
|
|
|
|
|
test items, read their marks, and assert the coverage matrix is complete:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
capability cli mcp skill
|
|
|
|
|
|
bridge_up ✓ ✓ — (not required for skill)
|
|
|
|
|
|
bridge_status ✓ ✓ ✓
|
|
|
|
|
|
catalog_list_targets ✓ ✓ —
|
|
|
|
|
|
...
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Fails with a table of gaps. The meta-test is itself validated by a fixture
|
|
|
|
|
|
that injects a synthetic `Capability("test_sentinel", frozenset({"cli","mcp"}))`,
|
|
|
|
|
|
deliberately omits the `mcp` test, and asserts the checker raises.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Phase 1 — Capability Registry
|
|
|
|
|
|
|
|
|
|
|
|
**Acceptance:** `from bridge.capabilities import CAPABILITIES` works; every
|
|
|
|
|
|
existing CLI command and the planned MCP tool set appears in the registry.
|
|
|
|
|
|
|
|
|
|
|
|
### T01 — Define capability registry module (src/bridge/capabilities.py)
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T01
|
|
|
|
|
|
state_hub_task_id: 1397a838-b225-4452-ad53-29ad65388060
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: high
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`Capability` dataclass with `name`, `description`, `required_access_modes`.
|
|
|
|
|
|
List all 10 capabilities as shown in the architecture above. No external
|
|
|
|
|
|
dependencies — pure stdlib.
|
|
|
|
|
|
|
|
|
|
|
|
### T02 — Meta-test: registry completeness against CLI commands and MCP tools
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T02
|
|
|
|
|
|
state_hub_task_id: 97467243-9237-4e63-a860-cc49587546ad
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: high
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Introspect `app.registered_commands` (Typer) and `mcp.list_tools()` (FastMCP).
|
|
|
|
|
|
Assert every name appears in `{c.name for c in CAPABILITIES}`. Fails fast if
|
|
|
|
|
|
a developer adds a CLI command or MCP tool without updating the registry.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Phase 2 — MCP Server
|
|
|
|
|
|
|
|
|
|
|
|
**Acceptance:** `uv run python src/bridge/mcp_server/server.py` starts without
|
|
|
|
|
|
error; `bridge_status()` returns a list of tunnel dicts; `bridge_up("x")`
|
|
|
|
|
|
returns `{"started": ["x"]}` or `{"already_running": ["x"]}`.
|
|
|
|
|
|
|
|
|
|
|
|
### T03 — Add fastmcp dependency and mcp_server package skeleton
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T03
|
|
|
|
|
|
state_hub_task_id: f2fd64f5-31c6-493b-b48b-d13980467cca
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: high
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Add `fastmcp>=2.0.0` to `[project.dependencies]` in `pyproject.toml`. Create
|
|
|
|
|
|
`src/bridge/mcp_server/__init__.py` (empty) and `server.py` with:
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
from fastmcp import FastMCP
|
|
|
|
|
|
mcp = FastMCP(name="ops-bridge", instructions="...")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
mcp.run(transport="stdio")
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### T04 — Implement bridge lifecycle MCP tools (up, down, restart, status, logs)
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T04
|
|
|
|
|
|
state_hub_task_id: 1bfc9b36-2be3-4606-a6e9-d611d1ac33ab
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: high
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`@mcp.tool()` wrappers that import and call the Python library directly (no
|
|
|
|
|
|
subprocess). Signatures:
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
def bridge_up(tunnel: str | None = None) -> dict
|
|
|
|
|
|
def bridge_down(tunnel: str | None = None) -> dict
|
|
|
|
|
|
def bridge_restart(tunnel: str | None = None) -> dict
|
|
|
|
|
|
def bridge_status() -> list[dict]
|
|
|
|
|
|
def bridge_logs(tunnel: str, lines: int = 50) -> list[dict]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
All return JSON-serialisable dicts/lists. `tunnel=None` means all tunnels.
|
|
|
|
|
|
|
|
|
|
|
|
### T05 — Implement catalog MCP tools
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T05
|
|
|
|
|
|
state_hub_task_id: ef7fa23c-d2e1-4fe0-9e26-994c1a6ce1fb
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: high
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
def catalog_list_targets(domain: str | None = None) -> list[dict]
|
|
|
|
|
|
def catalog_show_target(target_id: str) -> dict | None
|
|
|
|
|
|
def catalog_list_domains() -> list[dict]
|
|
|
|
|
|
def catalog_validate() -> dict # {"valid": bool, "errors": list[str]}
|
|
|
|
|
|
def catalog_show_bridge(bridge_id: str) -> dict | None
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
When `catalog_path` is not configured in `tunnels.yaml`, return
|
|
|
|
|
|
`{"error": "catalog_path not configured"}` rather than raising.
|
|
|
|
|
|
|
|
|
|
|
|
### T06 — Implement bridge:// and catalog:// MCP resources
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T06
|
|
|
|
|
|
state_hub_task_id: 71c9ee45-6928-416c-b4f3-dfb785a0ec8f
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: medium
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
@mcp.resource("bridge://status")
|
|
|
|
|
|
def resource_bridge_status() -> str:
|
|
|
|
|
|
"""Live snapshot of all tunnel states."""
|
|
|
|
|
|
|
|
|
|
|
|
@mcp.resource("catalog://domains")
|
|
|
|
|
|
def resource_catalog_domains() -> str: ...
|
|
|
|
|
|
|
|
|
|
|
|
@mcp.resource("catalog://targets")
|
|
|
|
|
|
def resource_catalog_targets() -> str: ...
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Resources are for cheap orientation reads; tools are for actions and
|
|
|
|
|
|
parameterised queries. Both are needed.
|
|
|
|
|
|
|
|
|
|
|
|
### T07 — Add .mcp.json project-scope registration config
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T07
|
|
|
|
|
|
state_hub_task_id: 618c011d-bd1b-4c8f-8750-f3d2f9fcaf88
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: medium
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"mcpServers": {
|
|
|
|
|
|
"ops-bridge": {
|
|
|
|
|
|
"type": "stdio",
|
|
|
|
|
|
"command": "uv",
|
|
|
|
|
|
"args": ["run", "python", "src/bridge/mcp_server/server.py"],
|
|
|
|
|
|
"cwd": "/home/worsch/ops-bridge"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Project-scope: Claude Code sessions inside `ops-bridge/` get the tools
|
|
|
|
|
|
automatically. See T14 for user-scope (machine-global) registration.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Phase 3 — Skill
|
|
|
|
|
|
|
|
|
|
|
|
**Acceptance:** `/bridge-status` invoked in Claude Code runs the skill,
|
|
|
|
|
|
calls `bridge_status` MCP tool, and returns a natural-language health summary.
|
|
|
|
|
|
|
|
|
|
|
|
### T08 — Implement /bridge-status skill for human operators
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T08
|
|
|
|
|
|
state_hub_task_id: 2c070f34-12b5-4dd9-ab24-bb7b6836773c
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: medium
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Skill file at `~/.claude/plugins/ops-bridge/bridge-status.md`. Prompt instructs
|
|
|
|
|
|
Claude to:
|
|
|
|
|
|
1. Call `bridge_status` MCP tool
|
|
|
|
|
|
2. Report each tunnel: name, state (with colour hint), host, uptime
|
|
|
|
|
|
3. Flag any `degraded` or `failed` tunnels and suggest `bridge restart <name>`
|
|
|
|
|
|
4. If catalog is configured, offer `catalog_list_targets` for discovery context
|
|
|
|
|
|
|
|
|
|
|
|
Skill prompt **must** reference the canonical capability names (`bridge_status`,
|
|
|
|
|
|
`catalog_list_targets`) so `test_skill.py` can assert coverage statically.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Phase 4 — Cross-Access-Mode Test Suite
|
|
|
|
|
|
|
|
|
|
|
|
**Acceptance:** `uv run pytest` fails if any capability is missing a test for
|
|
|
|
|
|
any of its required access modes. The failure message is a capability × mode
|
|
|
|
|
|
gap matrix. The meta-test is itself verified by a synthetic failing fixture.
|
|
|
|
|
|
|
|
|
|
|
|
### T09 — CLI test layer: annotate existing tests with capability/access_mode marks
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T09
|
|
|
|
|
|
state_hub_task_id: a8f3f5fb-fcd6-47e9-aad5-85dc803f796d
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: high
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Retrofit `tests/test_cli.py` (and other CLI test files) with:
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
@pytest.mark.capability("bridge_up")
|
|
|
|
|
|
@pytest.mark.access_mode("cli")
|
|
|
|
|
|
def test_bridge_up_starts_tunnel(...): ...
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Every capability whose `required_access_modes` includes `"cli"` must have at
|
|
|
|
|
|
least one marked test in the CLI layer.
|
|
|
|
|
|
|
|
|
|
|
|
### T10 — MCP test layer: tests/test_mcp.py with FastMCP in-process test client
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T10
|
|
|
|
|
|
state_hub_task_id: acb7ada6-111d-4b8d-b201-45748c394c43
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: high
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Use FastMCP's `Client(mcp_app)` context manager (in-process, no network):
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
@pytest.mark.capability("bridge_up")
|
|
|
|
|
|
@pytest.mark.access_mode("mcp")
|
|
|
|
|
|
async def test_bridge_up_mcp(mcp_client, mock_tunnel_manager):
|
|
|
|
|
|
result = await mcp_client.call_tool("bridge_up", {"tunnel": "t1"})
|
|
|
|
|
|
assert result["started"] == ["t1"]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Cover: correct return schema, missing tunnel name handled, catalog tools
|
|
|
|
|
|
graceful when `catalog_path` unset, resource URIs return valid JSON.
|
|
|
|
|
|
|
|
|
|
|
|
### T11 — Skill test layer: tests/test_skill.py — static skill coverage lint
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T11
|
|
|
|
|
|
state_hub_task_id: 071adfa4-2ccb-466b-b298-35130876267f
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: medium
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Parse the skill markdown file. Assert:
|
|
|
|
|
|
- File is syntactically valid (frontmatter parseable)
|
|
|
|
|
|
- Each capability with `"skill"` in `required_access_modes` has its `name`
|
|
|
|
|
|
appearing in the skill body text
|
|
|
|
|
|
|
|
|
|
|
|
This is a static lint, not an LLM invocation — fast and deterministic.
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
@pytest.mark.access_mode("skill")
|
|
|
|
|
|
def test_skill_covers_required_capabilities():
|
|
|
|
|
|
skill_text = Path("~/.claude/plugins/ops-bridge/bridge-status.md").read_text()
|
|
|
|
|
|
for cap in CAPABILITIES:
|
|
|
|
|
|
if "skill" in cap.required_access_modes:
|
|
|
|
|
|
assert cap.name in skill_text, f"Skill missing capability: {cap.name}"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### T12 — Cross-mode completeness meta-test: tests/test_coverage_completeness.py
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T12
|
|
|
|
|
|
state_hub_task_id: f1277a48-1790-42bd-8c70-8ba10c68312b
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: critical
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
The centrepiece. Uses a pytest plugin (conftest hook or `pytest.ini`
|
|
|
|
|
|
`collect_ignore`) to collect all test items, read their marks, build the
|
|
|
|
|
|
coverage matrix, and assert completeness:
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
def test_all_capabilities_have_all_required_mode_tests(pytestconfig):
|
|
|
|
|
|
covered = collect_capability_coverage(pytestconfig)
|
|
|
|
|
|
gaps = []
|
|
|
|
|
|
for cap in CAPABILITIES:
|
|
|
|
|
|
for mode in cap.required_access_modes:
|
|
|
|
|
|
if (cap.name, mode) not in covered:
|
|
|
|
|
|
gaps.append(f" {cap.name:<30} {mode}")
|
|
|
|
|
|
if gaps:
|
|
|
|
|
|
pytest.fail("Missing capability × mode coverage:\n" + "\n".join(gaps))
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**Self-validation fixture:** a separate test injects a synthetic capability
|
|
|
|
|
|
`Capability("_test_sentinel", frozenset({"cli","mcp"}))` into a copy of
|
|
|
|
|
|
`CAPABILITIES`, provides only a `cli`-marked test for it, and asserts that
|
|
|
|
|
|
calling `collect_capability_coverage` on this patched set reports the `mcp`
|
|
|
|
|
|
gap.
|
|
|
|
|
|
|
|
|
|
|
|
### T13 — conftest.py: pytest marks registration and coverage collector helper
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T13
|
|
|
|
|
|
state_hub_task_id: c518662a-9a5b-40de-86f5-582a16489cd3
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: medium
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Register custom marks to silence `PytestUnknownMarkWarning`:
|
|
|
|
|
|
|
|
|
|
|
|
```toml
|
|
|
|
|
|
# pyproject.toml
|
|
|
|
|
|
[tool.pytest.ini_options]
|
|
|
|
|
|
markers = [
|
|
|
|
|
|
"capability(name): the bridge capability under test",
|
|
|
|
|
|
"access_mode(mode): access mode being tested (cli, mcp, skill)",
|
|
|
|
|
|
]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Implement `collect_capability_coverage(session_or_items)` in `conftest.py`
|
|
|
|
|
|
that walks collected items and returns `set[tuple[str, str]]` of
|
|
|
|
|
|
`(capability_name, access_mode)` pairs.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Phase 5 — Registration and Documentation
|
|
|
|
|
|
|
|
|
|
|
|
**Acceptance:** `python scripts/register_mcp.py` registers ops-bridge MCP at
|
|
|
|
|
|
user scope; `bridge --help` still works; `uv run pytest` passes.
|
|
|
|
|
|
|
|
|
|
|
|
### T14 — User-scope registration guide and patch script
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T14
|
|
|
|
|
|
state_hub_task_id: b86916ba-59f3-44c1-b874-8af92d30e470
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: medium
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`scripts/register_mcp.py` modelled on `state-hub/scripts/patch_mcp_cwd.py`:
|
|
|
|
|
|
reads `.mcp.json`, registers at user scope via `claude mcp add-json -s user`,
|
|
|
|
|
|
then patches `cwd` directly in `~/.claude.json`. Update `README.txt` with:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
MCP INTEGRATION
|
|
|
|
|
|
---------------
|
|
|
|
|
|
Project-scope (auto, inside ops-bridge/):
|
|
|
|
|
|
Already configured in .mcp.json.
|
|
|
|
|
|
|
|
|
|
|
|
User-scope (machine-global, any repo):
|
|
|
|
|
|
python scripts/register_mcp.py
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### T15 — Integration test: agent workflow (bridge_status → bridge_up → bridge_status)
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: BRIDGE-WP-0003-T15
|
|
|
|
|
|
state_hub_task_id: d826764f-e2f1-4f6a-842c-a1852a88b209
|
2026-03-12 11:33:16 +01:00
|
|
|
|
status: done
|
2026-03-12 09:36:19 +01:00
|
|
|
|
priority: medium
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
End-to-end MCP flow with mocked `TunnelManager`:
|
|
|
|
|
|
|
|
|
|
|
|
1. `bridge_status()` → all tunnels `stopped`
|
|
|
|
|
|
2. `bridge_up("test-tunnel")` → `{"started": ["test-tunnel"]}`
|
|
|
|
|
|
3. `bridge_status()` → `test-tunnel` now `connected`
|
|
|
|
|
|
|
|
|
|
|
|
Verifies the MCP layer correctly delegates to the library and state is
|
|
|
|
|
|
reflected. Marked `@pytest.mark.capability("bridge_up") @pytest.mark.access_mode("mcp")`.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Capability × Mode Coverage Target
|
|
|
|
|
|
|
|
|
|
|
|
| Capability | CLI | MCP | Skill |
|
|
|
|
|
|
|-------------------------|-----|-----|-------|
|
|
|
|
|
|
| bridge_up | ✓ | ✓ | |
|
|
|
|
|
|
| bridge_down | ✓ | ✓ | |
|
|
|
|
|
|
| bridge_restart | ✓ | ✓ | |
|
|
|
|
|
|
| bridge_status | ✓ | ✓ | ✓ |
|
|
|
|
|
|
| bridge_logs | ✓ | ✓ | |
|
|
|
|
|
|
| catalog_list_targets | ✓ | ✓ | |
|
|
|
|
|
|
| catalog_show_target | ✓ | ✓ | |
|
|
|
|
|
|
| catalog_list_domains | ✓ | ✓ | |
|
|
|
|
|
|
| catalog_validate | ✓ | ✓ | |
|
|
|
|
|
|
| catalog_show_bridge | ✓ | ✓ | |
|
|
|
|
|
|
|
|
|
|
|
|
The skill only requires `bridge_status` and `catalog_list_targets` — the
|
|
|
|
|
|
two capabilities needed for a health summary. All others are CLI+MCP only.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Deferred
|
|
|
|
|
|
|
|
|
|
|
|
- **FR-27–29** — Identity provider integration — separate workplan.
|
|
|
|
|
|
- **Skill coverage for lifecycle operations** — `/bridge-up`, `/bridge-down`
|
|
|
|
|
|
skills for human operators are low value; agents use MCP tools directly.
|
|
|
|
|
|
- **Remote MCP transport (SSE/HTTP)** — stdio is sufficient for local use;
|
|
|
|
|
|
remote transport is a future concern when ops-bridge runs on a headless node.
|