/**
* mcp/skills/getProcesses.ts — get_processes skill
*
* Lists currently running processes with CPU and memory usage, sorted by
* the specified field. Helps Claude answer "what is using my CPU/memory?"
*
* Platform strategy
* -----------------
* darwin `ps -eo pid,pcpu,rss,comm` — built-in, no extra packages needed
* win32 PowerShell Get-Process | ConvertTo-Json via -EncodedCommand
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/getProcesses.ts [cpu|memory|name] [limit]
*/
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: "get_processes" ,
description:
"Lists currently running processes with CPU percentage and memory usage " +
"(MB), sorted by the specified field. " +
"Use when the user reports high CPU or memory usage or wants to identify " +
"resource-heavy applications." ,
riskLevel: "low" ,
destructive: false ,
requiresConsent: false ,
supportsDryRun: false ,
affectedScope: [ "user" ],
auditRequired: false ,
schema: {
sortBy: z
. enum ([ "cpu" , "memory" , "name" ])
. optional ()
. describe ( "Sort field. Default: cpu (highest first)." ),
limit: z
. number ()
. int ()
. positive ()
. optional ()
. describe ( "Maximum number of processes to return. Default: 30." ),
},
} as const ;
// -- Types --------------------------------------------------------------------
interface ProcessEntry {
pid : number ;
name : string ;
cpu : number ; // percentage
memoryMb : number ; // megabytes (RSS)
}
// -- 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: 20 * 1024 * 1024 },
);
return stdout. trim ();
}
// -- darwin implementation ----------------------------------------------------
async function getProcessesDarwin () : Promise < ProcessEntry []> {
// ps -eo pid,pcpu,rss,comm
// pid — process ID
// pcpu — CPU % (instantaneous, sampled over last scheduling interval)
// rss — resident set size in KB
// comm — executable name (no args)
const { stdout } = await execAsync (
"ps -eo pid,pcpu,rss,comm 2>/dev/null" ,
{ maxBuffer: 10 * 1024 * 1024 },
);
return stdout
. trim ()
. split ( " \n " )
. slice ( 1 ) // remove header line
. flatMap (( line ) => {
const parts = line. trim (). split ( / \s + / );
if (parts. length < 4 ) return [];
const pid = parseInt (parts[ 0 ], 10 );
const cpu = parseFloat (parts[ 1 ]);
const rssKb = parseInt (parts[ 2 ], 10 );
// comm may contain path separators — keep only the basename
const fullComm = parts. slice ( 3 ). join ( " " );
const name = fullComm. split ( "/" ). at ( - 1 ) ?? fullComm;
if ( isNaN (pid)) return [];
return [{ pid, name, cpu, memoryMb: Math. round ((rssKb / 1024 ) * 10 ) / 10 }];
});
}
// -- win32 implementation -----------------------------------------------------
async function getProcessesWin32 () : Promise < ProcessEntry []> {
// CPU on Windows is cumulative seconds, not a live %. We report it as-is
// and label it "cpuTimeSeconds" in the name for transparency; the column
// is still called "cpu" to keep the schema consistent.
const ps = `
$ErrorActionPreference = 'SilentlyContinue'
Get-Process | ForEach-Object {
[PSCustomObject]@{
pid = [int]$_.Id
name = $_.ProcessName
cpu = [Math]::Round([double]($_.CPU ?? 0), 2)
memoryMb = [Math]::Round($_.WorkingSet64 / 1MB, 1)
}
} | ConvertTo-Json -Depth 2 -Compress` . trim ();
const raw = await runPS (ps);
const parsed = JSON . parse (raw) as ProcessEntry | ProcessEntry [];
return Array. isArray (parsed) ? parsed : [parsed];
}
// -- Sorting ------------------------------------------------------------------
function sortProcesses (
list : ProcessEntry [],
sortBy : "cpu" | "memory" | "name" ,
) : ProcessEntry [] {
const copy = [ ... list];
if (sortBy === "name" ) return copy. sort (( a , b ) => a.name. localeCompare (b.name));
if (sortBy === "memory" ) return copy. sort (( a , b ) => b.memoryMb - a.memoryMb);
return copy. sort (( a , b ) => b.cpu - a.cpu); // "cpu" default
}
// -- Exported run function ----------------------------------------------------
export async function run ({
sortBy = "cpu" ,
limit = 30 ,
} : {
sortBy ?: "cpu" | "memory" | "name" ;
limit ?: number ;
} = {}) {
const platform = os. platform ();
const all = platform === "win32"
? await getProcessesWin32 ()
: await getProcessesDarwin ();
const sorted = sortProcesses (all, sortBy);
const processes = sorted. slice ( 0 , limit);
return {
platform,
sortBy,
totalProcesses: all. length ,
returned: processes. length ,
note: platform === "win32"
? "cpu column shows cumulative CPU time in seconds (Windows limitation)"
: "cpu column shows instantaneous CPU percentage" ,
processes,
};
}
// -- CLI smoke test -----------------------------------------------------------
if (require.main === module ) {
const sortBy = (process.argv[ 2 ] as "cpu" | "memory" | "name" ) ?? "cpu" ;
const limit = parseInt (process.argv[ 3 ] ?? "10" , 10 );
run ({ sortBy, limit })
. then (( r ) => {
console. log ( ` \n ${ r . totalProcesses } processes — showing top ${ r . returned } by ${ r . sortBy } \n ` );
r.processes. forEach (( p ) =>
console. log (
` PID ${ String ( p . pid ). padStart ( 6 ) } CPU ${ String ( p . cpu ). padStart ( 7 ) } ` +
`MEM ${ String ( p . memoryMb ). padStart ( 8 ) } MB ${ p . name }` ,
),
);
})
. catch (( err : Error ) => { console. error (err.message); process. exit ( 1 ); });
}