Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion SPECS/ARCHIVE/INDEX.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# mcpbridge-wrapper Tasks Archive

**Last Updated:** 2026-03-01 (P1-T8 archived)
**Last Updated:** 2026-03-01 (P3-T11 archived)

## Archived Tasks

| Task ID | Folder | Archived | Verdict |
|---------|--------|----------|---------|
| P3-T11 | [P3-T11_Add_Stop_broker_service_control_button_to_Web_UI/](P3-T11_Add_Stop_broker_service_control_button_to_Web_UI/) | 2026-03-01 | PASS |
| P1-T8 | [P1-T8_Update_config_examples_for_broker_setup_first/](P1-T8_Update_config_examples_for_broker_setup_first/) | 2026-03-01 | PASS |
| P2-T6 | [P2-T6_Remove_legacy_broker_connect_and_broker_spawn_flags/](P2-T6_Remove_legacy_broker_connect_and_broker_spawn_flags/) | 2026-03-01 | PASS |
| P1-T7 | [P1-T7_Hide_README_version_badge_maintenance_note/](P1-T7_Hide_README_version_badge_maintenance_note/) | 2026-03-01 | PASS |
Expand Down Expand Up @@ -181,6 +182,7 @@

| File | Description |
|------|-------------|
| [REVIEW_p3_t11_webui_stop_control.md](_Historical/REVIEW_p3_t11_webui_stop_control.md) | Review report for P3-T11 |
| [REVIEW_p1_t8_config_broker_setup_first.md](_Historical/REVIEW_p1_t8_config_broker_setup_first.md) | Review report for P1-T8 |
| [REVIEW_P2-T5_webui_mismatch_warning.md](_Historical/REVIEW_P2-T5_webui_mismatch_warning.md) | Review report for P2-T5 |
| [REVIEW_P2-T4_broker_unavailable_error.md](_Historical/REVIEW_P2-T4_broker_unavailable_error.md) | Review report for P2-T4 |
Expand Down Expand Up @@ -548,3 +550,5 @@
| 2026-02-28 | P1-T1 | Archived REVIEW_p1_t1_version_badge_script_tests report |
| 2026-02-28 | P1-T2 | Archived Add_Xcode_26_4_known_issue_release_notes_link_to_README (PASS) |
| 2026-02-28 | P1-T2 | Archived REVIEW_p1_t2_xcode_26_4_known_issue_link report |
| 2026-03-01 | P3-T11 | Archived Add_Stop_broker_service_control_button_to_Web_UI (PASS) |
| 2026-03-01 | P3-T11 | Archived REVIEW_p3_t11_webui_stop_control report |
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# P3-T11 - Add Stop broker/service control button to Web UI

**Task ID:** P3-T11
**Priority:** P1
**Dependencies:** P2-T6
**Status:** Planned

## Goal

Add a dashboard control that allows users to request graceful shutdown of the running broker/service process from the Web UI when supported by the runtime mode.

## Problem Statement

The current Web UI is observability-only. Users can inspect health/metrics but cannot stop a long-running broker/service process from the dashboard. They must switch to terminal-based process management, which is slower and less discoverable.

## Deliverables

- `src/mcpbridge_wrapper/webui/server.py`
- Add control capability endpoint (`GET /api/control`).
- Add stop endpoint (`POST /api/control/stop`) guarded by auth.
- Support optional shutdown callback wiring and graceful deferred trigger.
- `src/mcpbridge_wrapper/__main__.py`
- In broker-daemon mode, wire Web UI stop callback to graceful process shutdown signaling.
- `src/mcpbridge_wrapper/webui/static/index.html`
- Add Stop button in header controls (hidden/disabled until capability confirms support).
- `src/mcpbridge_wrapper/webui/static/dashboard.js`
- Load control capability at startup.
- Show/hide/label Stop button based on capability.
- Add confirmation + stop request flow with UX state updates.
- `tests/unit/webui/test_server.py`
- Add endpoint tests for supported/unsupported stop-control paths.
- Verify auth still applies to control endpoints.

## Acceptance Criteria

- Dashboard exposes a Stop control only when backend reports stop capability.
- `POST /api/control/stop` returns accepted and triggers graceful broker shutdown in broker-daemon mode.
- Unsupported runtime modes return a clear non-2xx response for stop requests.
- Unit tests cover supported and unsupported stop-control behavior.
- Existing quality gates pass:
- `pytest`
- `ruff check src/`
- `mypy src/`
- `pytest --cov` (>=90%)

## Implementation Notes

- Keep control endpoints auth-protected via existing `_check_auth` path.
- Prefer deferred shutdown trigger (small async delay) so HTTP response can return before process begins teardown.
- Keep behavior explicit in API payloads (`can_stop`, `service_name`, accepted/rejected state).
- In non-broker-daemon flows, advertise no stop capability and reject stop requests with HTTP 409.

## Validation Plan

1. Add/adjust unit tests for Web UI control endpoints.
2. Run full quality gates listed above.
3. Create `SPECS/INPROGRESS/P3-T11_Validation_Report.md` with command outputs and verdict.

## Risks

- If shutdown is triggered synchronously, response delivery may race process termination.
- Non-daemon modes must not be accidentally terminated by dashboard controls.

## Out of Scope

- Start/Restart controls.
- Per-client session stop management.
- Remote process control beyond local wrapper process scope.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Validation Report: P3-T11 — Add Stop broker/service control button to Web UI

**Date:** 2026-03-01
**Verdict:** PASS

## Summary

Implemented a Web UI stop control path for broker-daemon runtime with explicit capability discovery:
- Backend now exposes control capability and stop endpoints.
- Dashboard now renders a Stop button only when stop is supported.
- Broker-daemon mode wires stop requests to graceful self-termination signaling.

## Delivered Changes

- Added control API and stop callback plumbing:
- `src/mcpbridge_wrapper/webui/server.py`
- Wired broker-daemon stop callback into dashboard startup:
- `src/mcpbridge_wrapper/__main__.py`
- Added dashboard control button and action handler:
- `src/mcpbridge_wrapper/webui/static/index.html`
- `src/mcpbridge_wrapper/webui/static/dashboard.js`
- `src/mcpbridge_wrapper/webui/static/dashboard.css`
- Added/updated tests for control endpoints and broker-daemon wiring:
- `tests/unit/webui/test_server.py`
- `tests/unit/test_main.py`

## Acceptance Criteria Check

- [x] Dashboard exposes a Stop control only when backend reports stop capability.
- [x] `POST /api/control/stop` returns accepted and triggers graceful broker shutdown in broker-daemon mode.
- [x] Unsupported runtime modes return a clear non-2xx response for stop requests.
- [x] Unit tests cover supported and unsupported stop-control behavior.
- [x] Quality gates pass: `pytest`, `ruff check src/`, `mypy src/`, `pytest --cov` (coverage >= 90%).

## Quality Gates

1. `pytest`
- Result: PASS
- Evidence: `740 passed, 5 skipped`

2. `ruff check src/`
- Result: PASS
- Evidence: `All checks passed!`

3. `mypy src/`
- Result: PASS
- Evidence: `Success: no issues found in 18 source files`

4. `pytest --cov`
- Result: PASS
- Evidence: `Required test coverage of 90.0% reached. Total coverage: 91.01%`

## Notes

- Stop capability is intentionally advertised only when `request_stop` callback is wired (broker-daemon mode).
- In unsupported modes, stop requests return HTTP 409 with actionable detail.
34 changes: 34 additions & 0 deletions SPECS/ARCHIVE/_Historical/REVIEW_p3_t11_webui_stop_control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## REVIEW REPORT — P3-T11 Web UI Stop Control

**Scope:** origin/main..HEAD
**Files:** 12

### Summary Verdict
- [x] Approve
- [ ] Approve with comments
- [ ] Request changes
- [ ] Block

### Critical Issues

- None.

### Secondary Issues

- None.

### Architectural Notes

- Control-plane API (`/api/control`, `/api/control/stop`) is capability-driven and opt-in via callback wiring, which avoids unsafe stop behavior in unsupported runtime modes.
- Broker-daemon stop is routed through delayed self-SIGTERM from a helper thread so HTTP response can complete before shutdown starts.

### Tests

- `pytest` passed (`740 passed, 5 skipped`)
- `ruff check src/` passed
- `mypy src/` passed
- `pytest --cov` passed (`91.01%`, threshold >= 90%)

### Next Steps

- FOLLOW-UP skipped: no actionable findings.
3 changes: 2 additions & 1 deletion SPECS/INPROGRESS/next.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# No Active Task

**Status:** Idle — P1-T8 archived. Select the next task from `SPECS/Workplan.md`.
**Status:** Idle — P3-T11 archived. Select the next task from `SPECS/Workplan.md`.

## Recently Archived

- **P3-T11** — Add Stop broker/service control button to Web UI (2026-03-01, PASS)
- **P1-T8** — Update /config examples for broker setup first (2026-03-01, PASS)
- **P2-T6** — Remove legacy --broker-connect and --broker-spawn flags (2026-03-01, PASS)
- **P1-T7** — Hide README version badge maintenance note (2026-03-01, PASS)
Expand Down
20 changes: 20 additions & 0 deletions SPECS/Workplan.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,26 @@ Add new tasks using the canonical template in [TASK_TEMPLATE.md](TASK_TEMPLATE.m
- [x] Broker mode guidance remains clear with `--broker` (proxy) and `--broker-daemon` (host)
- [x] Required quality gates pass (`pytest`, `ruff check src/`, `mypy src/`, `pytest --cov` with coverage >=90%)

### Phase 3: Web UI Controls

#### ✅ P3-T11: Add Stop broker/service control button to Web UI
- **Status:** ✅ Completed (2026-03-01)
- **Description:** Add a Web UI control that lets users request graceful shutdown of the running broker/service process directly from the dashboard, with clear availability rules and safe behavior in unsupported modes.
- **Priority:** P1
- **Dependencies:** P2-T6
- **Parallelizable:** yes
- **Outputs/Artifacts:**
- `src/mcpbridge_wrapper/webui/server.py` — control capability + stop endpoint
- `src/mcpbridge_wrapper/webui/static/index.html` — Stop control button
- `src/mcpbridge_wrapper/webui/static/dashboard.js` — control discovery + stop action handler
- `src/mcpbridge_wrapper/__main__.py` — broker-daemon shutdown wiring for Web UI control
- `tests/unit/webui/test_server.py` — endpoint and capability tests
- **Acceptance Criteria:**
- [x] Dashboard exposes a Stop control only when backend reports stop capability
- [x] `POST /api/control/stop` returns accepted and triggers graceful broker shutdown in broker-daemon mode
- [x] Unsupported runtime modes return a clear non-2xx response for stop requests
- [x] Unit tests cover both supported and unsupported stop-control paths

### Bug Fixes

#### ✅ BUG-T8: Fix broker proxy bridge exits after first write due to BaseProtocol missing _drain_helper
Expand Down
20 changes: 19 additions & 1 deletion src/mcpbridge_wrapper/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,8 @@ def main() -> int:
from mcpbridge_wrapper.broker.transport import UnixSocketServer
from mcpbridge_wrapper.broker.types import BrokerConfig

stop_requested = threading.Event()
daemon: Optional[BrokerDaemon] = None
config = None
metrics = None
audit = None
Expand Down Expand Up @@ -534,14 +536,30 @@ def main() -> int:
file=sys.stderr,
)
else:
_ = run_server_in_thread(config, metrics, audit)

def request_broker_shutdown() -> None:
"""Request broker daemon shutdown after replying to HTTP control call."""
stop_requested.set()
if daemon is not None:
daemon.request_shutdown()

_ = run_server_in_thread(
config,
metrics,
audit,
service_name="broker-daemon",
request_stop=request_broker_shutdown,
)
print(
f"Web UI dashboard started at http://{config.host}:{config.port}",
file=sys.stderr,
)

broker_config = BrokerConfig.default()
daemon = BrokerDaemon(broker_config)
if stop_requested.is_set():
daemon.request_shutdown()

transport = UnixSocketServer(
broker_config,
daemon,
Expand Down
56 changes: 42 additions & 14 deletions src/mcpbridge_wrapper/broker/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import os
import signal
import sys
import threading
from asyncio.subprocess import PIPE
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -73,6 +74,11 @@ def __init__(
self._reconnect_attempt: int = 0
self._stop_event: asyncio.Event = asyncio.Event()
self._stopped_event: asyncio.Event = asyncio.Event()
# When set, run_forever should stop as soon as startup reaches READY.
self._shutdown_requested: bool = False
# Event loop running run_forever; used for thread-safe stop scheduling.
self._loop: asyncio.AbstractEventLoop | None = None
self._shutdown_lock = threading.Lock()

# ------------------------------------------------------------------
# Public API
Expand All @@ -95,6 +101,28 @@ def status(self) -> dict[str, Any]:
"upstream_pid": upstream_pid,
}

def request_shutdown(self) -> None:
"""Request graceful daemon shutdown from any thread/context.

This method is safe to call before :meth:`run_forever` starts. In that
case the request is recorded and applied immediately after startup.
"""
with self._shutdown_lock:
self._shutdown_requested = True

loop = self._loop
if loop is None or not loop.is_running():
return

def _schedule_stop() -> None:
# During startup (INIT), defer actual stop to run_forever() post-start check.
if self._state == BrokerState.INIT:
return
asyncio.ensure_future(self.stop())

with contextlib.suppress(RuntimeError):
loop.call_soon_threadsafe(_schedule_stop)

async def start(self) -> None:
"""Start the broker: validate lock, launch upstream, then write PID file.

Expand Down Expand Up @@ -201,28 +229,28 @@ async def stop(self) -> None:

async def run_forever(self) -> None:
"""Start and block until a shutdown signal is received."""
await self.start()

loop = asyncio.get_running_loop()

shutdown_called = False

async def _handle_signal() -> None:
nonlocal shutdown_called
if not shutdown_called:
shutdown_called = True
await self.stop()
self._loop = loop

def _sync_signal_handler() -> None:
asyncio.ensure_future(_handle_signal())
self.request_shutdown()

for sig in (signal.SIGTERM, signal.SIGINT):
with contextlib.suppress(NotImplementedError, RuntimeError):
loop.add_signal_handler(sig, _sync_signal_handler)

# Wait for shutdown to be requested and fully completed.
await self._stop_event.wait()
await self._stopped_event.wait()
try:
await self.start()

# Handle stop requests that happened before or during startup.
if self._shutdown_requested:
await self.stop()

# Wait for shutdown to be requested and fully completed.
await self._stop_event.wait()
await self._stopped_event.wait()
finally:
self._loop = None

# ------------------------------------------------------------------
# Internal helpers
Expand Down
Loading