/**
* mcp/skills/killProcess.ts — kill_process skill
*
* Terminates a process by name or PID. Defaults to dry-run to prevent
* accidental termination of critical system processes.
*
* Platform strategy
* -----------------
* darwin pkill / kill builtins; pgrep for dry-run matching
* win32 PowerShell Get-Process | Stop-Process
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/killProcess.ts
*/
import * as os from "os" ;
import { exec } from "child_process" ;
import { promisify } from "util" ;
import { z } from "zod" ;
const execAsync = promisify (exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "kill_process" ,
description:
"Terminates a process by name or PID. Use when a process is unresponsive " +
"or consuming excessive resources. Always confirm with user before killing " +
"critical system processes." ,
riskLevel: "medium" ,
destructive: true ,
requiresConsent: true ,
supportsDryRun: true ,
affectedScope: [ "user" ],
auditRequired: true ,
schema: {
name: z
. string ()
. optional ()
. describe ( "Process name to kill (case-insensitive partial match)" ),
pid: z
. number ()
. optional ()
. describe ( "Exact PID to kill. More precise than name" ),
signal: z
. enum ([ "TERM" , "KILL" ])
. optional ()
. describe ( "Signal to send. TERM=graceful shutdown, KILL=force. Default: TERM" ),
dryRun: z
. boolean ()
. optional ()
. describe ( "If true, show what would be killed without killing. Default: true" ),
},
} as const ;
// -- Types --------------------------------------------------------------------
interface MatchedProcess {
pid : number ;
name : string ;
}
interface KillResult {
matched : MatchedProcess [];
killed : boolean ;
dryRun : boolean ;
signal : string ;
message : string ;
}
// -- Guards -------------------------------------------------------------------
const PROTECTED_NAMES = new Set ([ "kernel" , "launchd" , "systemd" ]);
function isProtected ( pid : number | undefined , name : string | undefined ) : boolean {
if (pid === 1 ) return true ;
if (name && PROTECTED_NAMES . has (name. toLowerCase ())) return true ;
return false ;
}
// -- PowerShell helper --------------------------------------------------------
async function runPS ( script : string ) : Promise < string > {
const encoded = Buffer. from (script, "utf16le" ). toString ( "base64" );
const { stdout } = await execAsync (
`powershell.exe -NoProfile -NonInteractive -EncodedCommand ${ encoded }` ,
{ maxBuffer: 10 * 1024 * 1024 },
);
return stdout. trim ();
}
// -- darwin implementation ----------------------------------------------------
async function killProcessDarwin (
name : string | undefined ,
pid : number | undefined ,
signal : string ,
dryRun : boolean ,
) : Promise < KillResult > {
const matched : MatchedProcess [] = [];
if (pid !== undefined ) {
// Resolve process name for the given PID
try {
const { stdout } = await execAsync (
`ps -p ${ pid } -o comm= 2>/dev/null` ,
{ maxBuffer: 1024 * 1024 },
);
const procName = stdout. trim (). split ( "/" ). at ( - 1 ) ?? "" ;
if (procName) matched. push ({ pid, name: procName });
} catch {
// process may not exist
}
} else if (name) {
const safeName = name. replace ( / ' / g , `' \\ ''` );
try {
const { stdout } = await execAsync (
`pgrep -i -l '${ safeName }' 2>/dev/null` ,
{ maxBuffer: 1024 * 1024 },
);
for ( const line of stdout. trim (). split ( " \n " ). filter (Boolean)) {
const parts = line. trim (). split ( / \s + / );
const p = parseInt (parts[ 0 ], 10 );
const n = parts. slice ( 1 ). join ( " " ). split ( "/" ). at ( - 1 ) ?? parts[ 1 ] ?? "" ;
if ( ! isNaN (p)) matched. push ({ pid: p, name: n });
}
} catch {
// no matches
}
}
if (matched. length === 0 ) {
return { matched, killed: false , dryRun, signal, message: "No matching processes found." };
}
// Check for protected processes
for ( const m of matched) {
if ( isProtected (m.pid, m.name)) {
return {
matched,
killed: false ,
dryRun,
signal,
message: `Refused: process '${ m . name }' (PID ${ m . pid }) is a protected system process.` ,
};
}
}
if (dryRun) {
return {
matched,
killed: false ,
dryRun: true ,
signal,
message: `Dry run: would send SIG${ signal } to ${ matched . length } process(es).` ,
};
}
try {
if (pid !== undefined ) {
await execAsync ( `kill -${ signal } ${ pid } 2>/dev/null` );
} else if (name) {
const safeName = name. replace ( / ' / g , `' \\ ''` );
await execAsync ( `pkill -${ signal } -i '${ safeName }' 2>/dev/null` );
}
return {
matched,
killed: true ,
dryRun: false ,
signal,
message: `Sent SIG${ signal } to ${ matched . length } process(es).` ,
};
} catch (err) {
const msg = (err as Error ).message ?? String (err);
return { matched, killed: false , dryRun: false , signal, message: `Kill failed: ${ msg }` };
}
}
// -- win32 implementation -----------------------------------------------------
async function killProcessWin32 (
name : string | undefined ,
pid : number | undefined ,
signal : string ,
dryRun : boolean ,
) : Promise < KillResult > {
const force = signal === "KILL" ? "-Force" : "" ;
let listPs : string ;
if (pid !== undefined ) {
listPs = `Get-Process -Id ${ pid } -ErrorAction SilentlyContinue | Select-Object Id,ProcessName | ConvertTo-Json -Depth 1 -Compress` ;
} else {
const safeName = (name ?? "" ). replace ( / ' / g , "''" );
listPs = `Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -like '*${ safeName }*' } | Select-Object Id,ProcessName | ConvertTo-Json -Depth 1 -Compress` ;
}
const raw = await runPS (listPs);
if ( ! raw) {
return { matched: [], killed: false , dryRun, signal, message: "No matching processes found." };
}
const parsed = JSON . parse (raw) as { Id : number ; ProcessName : string } | { Id : number ; ProcessName : string }[];
const arr = Array. isArray (parsed) ? parsed : [parsed];
const matched : MatchedProcess [] = arr. map ( p => ({ pid: p.Id, name: p.ProcessName }));
for ( const m of matched) {
if ( isProtected (m.pid, m.name)) {
return {
matched,
killed: false ,
dryRun,
signal,
message: `Refused: process '${ m . name }' (PID ${ m . pid }) is a protected system process.` ,
};
}
}
if (dryRun) {
return {
matched,
killed: false ,
dryRun: true ,
signal,
message: `Dry run: would stop ${ matched . length } process(es).` ,
};
}
let stopPs : string ;
if (pid !== undefined ) {
stopPs = `Stop-Process -Id ${ pid } ${ force } -ErrorAction SilentlyContinue` ;
} else {
const safeName = (name ?? "" ). replace ( / ' / g , "''" );
stopPs = `Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -like '*${ safeName }*' } | Stop-Process ${ force } -ErrorAction SilentlyContinue` ;
}
try {
await runPS (stopPs);
return { matched, killed: true , dryRun: false , signal, message: `Stopped ${ matched . length } process(es).` };
} catch (err) {
const msg = (err as Error ).message ?? String (err);
return { matched, killed: false , dryRun: false , signal, message: `Stop failed: ${ msg }` };
}
}
// -- Exported run function ----------------------------------------------------
export async function run ({
name ,
pid ,
signal = "TERM" ,
dryRun = true ,
} : {
name ?: string ;
pid ?: number ;
signal ?: "TERM" | "KILL" ;
dryRun ?: boolean ;
} = {}) : Promise < KillResult > {
if ( ! name && pid === undefined ) {
throw new Error ( "[kill_process] Must provide either 'name' or 'pid'." );
}
const platform = os. platform ();
if (platform === "win32" ) {
return killProcessWin32 (name, pid, signal, dryRun);
}
return killProcessDarwin (name, pid, signal, dryRun);
}
// -- Smoke test ---------------------------------------------------------------
if ( false ) {
run ({})
. then ( r => console. log ( JSON . stringify (r, null , 2 )))
. catch (( err : Error ) => { console. error (err.message); process. exit ( 1 ); });
}