/**
* mcp/skills/getTopConsumers.ts — get_top_consumers skill
*
* Returns processes ranked by combined CPU and memory consumption. Provides a
* quick snapshot of what is most impacting system performance.
*
* Platform strategy
* -----------------
* darwin `ps -eo pid,pcpu,rss,comm` — parse and rank by combined score
* win32 PowerShell Get-Process | Sort-Object CPU -Descending
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/getTopConsumers.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: "get_top_consumers" ,
description:
"Returns processes ranked by combined CPU and memory consumption. Provides " +
"a quick snapshot of what is most impacting system performance. Use when " +
"diagnosing slowness without a specific process in mind." ,
riskLevel: "low" ,
destructive: false ,
requiresConsent: false ,
supportsDryRun: false ,
affectedScope: [ "user" ],
auditRequired: false ,
schema: {
limit: z
. number ()
. optional ()
. describe ( "Number of top processes to return. Default: 10" ),
metric: z
. enum ([ "cpu" , "memory" , "combined" ])
. optional ()
. describe ( "Ranking metric. Default: combined" ),
},
} as const ;
// -- Types --------------------------------------------------------------------
interface ConsumerEntry {
pid : number ;
name : string ;
cpuPercent : number ;
memoryMb : number ;
combinedScore : number ;
}
// -- 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 getTopConsumersDarwin (
limit : number ,
metric : "cpu" | "memory" | "combined" ,
) : Promise < ConsumerEntry []> {
const { stdout } = await execAsync (
"ps -eo pid,pcpu,rss,comm 2>/dev/null" ,
{ maxBuffer: 10 * 1024 * 1024 },
);
const rows = stdout
. trim ()
. split ( " \n " )
. slice ( 1 )
. 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 );
const fullComm = parts. slice ( 3 ). join ( " " );
const name = fullComm. split ( "/" ). at ( - 1 ) ?? fullComm;
if ( isNaN (pid)) return [];
const memoryMb = Math. round ((rssKb / 1024 ) * 10 ) / 10 ;
return [{ pid, name, cpuPercent: cpu, memoryMb, combinedScore: 0 }];
});
// Normalise and compute combined score
const maxCpu = Math. max ( ... rows. map ( r => r.cpuPercent), 1 );
const maxMem = Math. max ( ... rows. map ( r => r.memoryMb), 1 );
for ( const r of rows) {
r.combinedScore = Math. round (
((r.cpuPercent / maxCpu) * 50 + (r.memoryMb / maxMem) * 50 ) * 100 ,
) / 100 ;
}
const sortKey : keyof ConsumerEntry =
metric === "cpu" ? "cpuPercent" :
metric === "memory" ? "memoryMb" : "combinedScore" ;
return rows
. sort (( a , b ) => (b[sortKey] as number ) - (a[sortKey] as number ))
. slice ( 0 , limit);
}
// -- win32 implementation -----------------------------------------------------
async function getTopConsumersWin32 (
limit : number ,
metric : "cpu" | "memory" | "combined" ,
) : Promise < ConsumerEntry []> {
const sortProp = metric === "memory" ? "WorkingSet64" : "CPU" ;
const ps = `
$ErrorActionPreference = 'SilentlyContinue'
Get-Process | Sort-Object ${ sortProp } -Descending | Select-Object -First ${ limit } | ForEach-Object {
[PSCustomObject]@{
pid = [int]$_.Id
name = $_.ProcessName
cpuPercent = [Math]::Round([double]($_.CPU ?? 0), 2)
memoryMb = [Math]::Round($_.WorkingSet64 / 1MB, 1)
}
} | ConvertTo-Json -Depth 2 -Compress` . trim ();
const raw = await runPS (ps);
if ( ! raw) return [];
const parsed = JSON . parse (raw) as Omit < ConsumerEntry , "combinedScore" >[] | Omit < ConsumerEntry , "combinedScore" >;
const arr = Array. isArray (parsed) ? parsed : [parsed];
const maxCpu = Math. max ( ... arr. map ( r => r.cpuPercent), 1 );
const maxMem = Math. max ( ... arr. map ( r => r.memoryMb), 1 );
return arr. map ( r => ({
... r,
combinedScore: Math. round (
((r.cpuPercent / maxCpu) * 50 + (r.memoryMb / maxMem) * 50 ) * 100 ,
) / 100 ,
}));
}
// -- Exported run function ----------------------------------------------------
export async function run ({
limit = 10 ,
metric = "combined" ,
} : {
limit ?: number ;
metric ?: "cpu" | "memory" | "combined" ;
} = {}) {
const platform = os. platform ();
const processes = platform === "win32"
? await getTopConsumersWin32 (limit, metric)
: await getTopConsumersDarwin (limit, metric);
return {
platform,
metric,
processes,
sampledAt: new Date (). toISOString (),
};
}
// -- Smoke test ---------------------------------------------------------------
if ( false ) {
run ({})
. then ( r => console. log ( JSON . stringify (r, null , 2 )))
. catch (( err : Error ) => { console. error (err.message); process. exit ( 1 ); });
}