Skip to content

Commit 4545bba

Browse files
drknowhowclaude
andcommitted
Add Force Restart to extension Settings and launcher
launcher.py: - _kill_port(port): kills any process listening on a port via netstat+powershell (Win) or lsof+kill (Unix) - cmd_force_restart(): kills PID-tracked server + anything holding port 18925, starts fresh - 'force_restart' added to command dispatch table extension: - FORCE_RESTART_MCP message type in types.ts - background.ts routes FORCE_RESTART_MCP -> 'force_restart' native message - useMcpLauncher: forceRestart() action - MCP Bridge UI: amber "↺ Restart" button visible when launcher is installed Kills stale/competing servers on port 18925 and starts a clean one Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ed21c6a commit 4545bba

4 files changed

Lines changed: 79 additions & 8 deletions

File tree

extension/src/background.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,11 +617,13 @@ onMessage((message: Message, _sender, sendResponse) => {
617617
case 'STOP_MCP_SERVER':
618618
case 'MCP_LAUNCHER_STATUS':
619619
case 'REMOVE_MCP_LAUNCHER':
620-
case 'INSTALL_LOCAL_MCP': {
620+
case 'INSTALL_LOCAL_MCP':
621+
case 'FORCE_RESTART_MCP': {
621622
const cmd = type === 'LAUNCH_MCP_SERVER' ? 'start'
622623
: type === 'STOP_MCP_SERVER' ? 'stop'
623624
: type === 'REMOVE_MCP_LAUNCHER'? 'uninstall'
624625
: type === 'INSTALL_LOCAL_MCP' ? 'install_local'
626+
: type === 'FORCE_RESTART_MCP' ? 'force_restart'
625627
: 'status';
626628
const nativePayload = type === 'INSTALL_LOCAL_MCP'
627629
? { command: cmd, payload: { project_path: (payload as Record<string, string>).projectPath } }

extension/src/components/SettingsPanel.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,16 @@ function useMcpLauncher() {
145145
} finally { setBusy(false); }
146146
};
147147

148-
return { status, busy, start, stop, remove, refresh: query };
148+
const forceRestart = async () => {
149+
setBusy(true);
150+
try {
151+
const res = await chrome.runtime.sendMessage({ type: 'FORCE_RESTART_MCP' });
152+
if (res?.notInstalled) persist('not_installed');
153+
else if (res?.success) persist('running');
154+
} finally { setBusy(false); }
155+
};
156+
157+
return { status, busy, start, stop, remove, forceRestart, refresh: query };
149158
}
150159

151160
// ── MCP Server Card ──
@@ -574,7 +583,7 @@ function AiToolsSection() {
574583

575584
export default function SettingsPanel() {
576585
const { status: wsStatus, refresh: refreshWs, reconnect: reconnectWs } = useMcpStatus();
577-
const { status: launchStatus, busy, start, stop, remove, refresh: refreshLauncher } = useMcpLauncher();
586+
const { status: launchStatus, busy, start, stop, remove, forceRestart, refresh: refreshLauncher } = useMcpLauncher();
578587
const [extId] = useState(() => chrome.runtime.id);
579588
const [idCopied, setIdCopied] = useState(false);
580589

@@ -679,6 +688,16 @@ export default function SettingsPanel() {
679688
</>
680689
)
681690
)}
691+
{launchStatus !== 'not_installed' && launchStatus !== 'unknown' && (
692+
<button
693+
onClick={() => { forceRestart(); setTimeout(() => { refreshWs(); refreshLauncher(); }, 2000); }}
694+
disabled={busy}
695+
className="px-2 py-1 text-[10px] font-semibold text-amber-600 hover:text-amber-700 border border-amber-200 bg-amber-50 hover:bg-amber-100 disabled:opacity-40 rounded transition-colors"
696+
title="Kill any stale server on port 18925 and start fresh"
697+
>
698+
{busy ? '…' : '↺ Restart'}
699+
</button>
700+
)}
682701
<button
683702
onClick={() => { refreshWs(); refreshLauncher(); }}
684703
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"

extension/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export type MessageType =
7676
| 'MCP_LAUNCHER_STATUS'
7777
| 'REMOVE_MCP_LAUNCHER'
7878
| 'INSTALL_LOCAL_MCP'
79+
| 'FORCE_RESTART_MCP'
7980
// ── API (MCP WebSocket bridge) ──
8081
| 'API_NAVIGATE'
8182
| 'API_SCREENSHOT'

mcp-server/launcher.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,54 @@ def cmd_install_local(payload: dict) -> dict:
143143
return {'success': False, 'error': str(e)}
144144

145145

146+
def _kill_port(port: int) -> list[int]:
147+
"""Kill any process listening on the given port. Returns list of killed PIDs."""
148+
killed = []
149+
try:
150+
if sys.platform == 'win32':
151+
out = subprocess.run(['netstat', '-ano'], capture_output=True, text=True).stdout
152+
for line in out.splitlines():
153+
if f'127.0.0.1:{port}' in line and 'LISTENING' in line:
154+
pid = int(line.split()[-1])
155+
subprocess.run(
156+
['powershell', '-Command', f'Stop-Process -Id {pid} -Force -ErrorAction SilentlyContinue'],
157+
capture_output=True,
158+
)
159+
killed.append(pid)
160+
else:
161+
out = subprocess.run(['lsof', '-ti', f'tcp:{port}'], capture_output=True, text=True).stdout
162+
for pid_str in out.strip().splitlines():
163+
pid = int(pid_str)
164+
os.kill(pid, 9)
165+
killed.append(pid)
166+
except Exception:
167+
pass
168+
return killed
169+
170+
171+
def cmd_force_restart() -> dict:
172+
"""Kill every process holding port 18925 (including stale --ws-only servers
173+
and any competing MCP instances) then start a fresh --ws-only server."""
174+
import time
175+
176+
# Kill the PID-tracked server if any
177+
pid = read_pid()
178+
if pid:
179+
_kill(pid)
180+
if os.path.exists(PID_FILE):
181+
os.unlink(PID_FILE)
182+
183+
# Force-kill anything else holding the port
184+
killed = _kill_port(18925)
185+
186+
# Brief pause for the port to release
187+
if killed:
188+
time.sleep(0.8)
189+
190+
# Start fresh
191+
return cmd_start()
192+
193+
146194
def cmd_uninstall() -> dict:
147195
"""Remove the native host registration and generated files."""
148196
HOST_NAME = 'com.sentinel.launcher'
@@ -184,11 +232,12 @@ def main() -> None:
184232
cmd = msg.get('command', '')
185233
payload = msg.get('payload', {})
186234
handlers = {
187-
'start': lambda: cmd_start(),
188-
'stop': lambda: cmd_stop(),
189-
'status': lambda: cmd_status(),
190-
'uninstall': lambda: cmd_uninstall(),
191-
'install_local': lambda: cmd_install_local(payload),
235+
'start': lambda: cmd_start(),
236+
'stop': lambda: cmd_stop(),
237+
'status': lambda: cmd_status(),
238+
'force_restart': lambda: cmd_force_restart(),
239+
'uninstall': lambda: cmd_uninstall(),
240+
'install_local': lambda: cmd_install_local(payload),
192241
}
193242
handler = handlers.get(cmd)
194243
if handler:

0 commit comments

Comments
 (0)