Browse Source

MCP stop closes client or window gracefully

master
Serge Shutov 1 week ago
parent
commit
930560ca4f
  1. 59
      bridge/mcp-tools.js
  2. 2
      bridge/server.js

59
bridge/mcp-tools.js

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
const net = require('net');
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const { spawn, execFile } = require('child_process');
const { McpClient } = require('./mcp-client');
const { buildSpecs, guiLaunchArgs } = require('./mcp-registry');
@ -25,6 +25,28 @@ function checkPort(port, timeoutMs) { @@ -25,6 +25,28 @@ function checkPort(port, timeoutMs) {
});
}
function findPidByPort(port) {
return new Promise((resolve) => {
if (port == null) return resolve(null);
const ps = 'Get-NetTCPConnection -LocalPort ' + Number(port) + ' -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty OwningProcess';
execFile('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ps], { timeout: 8000, windowsHide: true }, (err, stdout) => {
if (err) return resolve(null);
const pid = parseInt(String(stdout).trim(), 10);
resolve(Number.isFinite(pid) && pid > 0 ? pid : null);
});
});
}
function softKill(pid) {
return new Promise((resolve) => {
if (!pid) return resolve({ ok: false, reason: 'no pid' });
// No /F: sends WM_CLOSE so the app can ask to save.
execFile('taskkill.exe', ['/PID', String(pid)], { timeout: 8000, windowsHide: true }, (err, stdout, stderr) => {
resolve({ ok: !err, out: String(stdout || '').trim(), err: String(stderr || '').trim() });
});
});
}
async function waitPort(port, totalMs) {
const deadline = Date.now() + (totalMs || 15000);
while (Date.now() < deadline) {
@ -227,13 +249,42 @@ class McpManager { @@ -227,13 +249,42 @@ class McpManager {
try { return await p; } finally { delete this.launching[name]; }
}
// Закрыть MCP-клиент приложения (окно не трогаем).
stop(name) {
// Закрыть MCP-клиент; если клиента нет, но окно открыто — мягко закрыть окно.
async stop(name) {
const spec = this._spec(name);
const c = this.clients[name];
const clientStopped = !!c;
if (c) { try { c.close(); } catch {} delete this.clients[name]; }
delete this.starting[name];
this.lastError[name] = null;
return { app: name, stopped: true };
const guiUp = spec.port != null ? await checkPort(spec.port, 600) : false;
if (!guiUp) {
return { app: name, stopped: clientStopped, windowClosed: false };
}
const pid = await findPidByPort(spec.port);
if (!pid) {
return { app: name, stopped: clientStopped, windowClosed: false, warning: 'port is open but no owning process found' };
}
console.log('[mcp] closing ' + name + ' window, pid ' + pid + ' (port ' + spec.port + ')');
const r = await softKill(pid);
if (!r.ok) {
this.lastError[name] = 'close window failed: ' + (r.err || 'unknown');
return { app: name, stopped: clientStopped, windowClosed: false, error: r.err || 'taskkill failed' };
}
const closed = await new Promise((resolve) => {
const t0 = Date.now();
const iv = setInterval(async () => {
if (!(await checkPort(spec.port, 500))) { clearInterval(iv); resolve(true); }
else if (Date.now() - t0 > 5000) { clearInterval(iv); resolve(false); }
}, 400);
});
if (!closed) {
this.lastError[name] = 'window did not close in 5s (app may be waiting to save)';
return { app: name, stopped: clientStopped, windowClosed: false, warning: 'app did not close within 5s, it may be asking to save' };
}
console.log('[mcp] ' + name + ' window closed');
return { app: name, stopped: clientStopped, windowClosed: true };
}
toolDefs() {

2
bridge/server.js

@ -167,7 +167,7 @@ async function handle(req, res, pathname, body) { @@ -167,7 +167,7 @@ async function handle(req, res, pathname, body) {
if (req.method !== 'POST') throw new ToolError('E_ARGS_INVALID', 'only POST');
const app = body && body.app;
if (!app || typeof app !== 'string') throw new ToolError('E_ARGS_INVALID', 'missing app');
return { ok: true, ...mcp.stop(app) };
return { ok: true, ...(await mcp.stop(app)) };
}
case '/tool': {
const { tool, args, workspace, confirmToken } = body || {};

Loading…
Cancel
Save