feat: implement v1.0 high-priority tools (14 tools)
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled

Replace template API scaffolding with Windows desktop system tools:

- shell.ts: PowerShell/cmd/bash executor + persistent terminal sessions
- tools/execute.ts: windows_execute (#1)
- tools/process.ts: windows_process_list (#2)
- tools/audio.ts: windows_audio_get, windows_audio_set (#6, #7)
- tools/system.ts: windows_system_info (#18)
- tools/terminal.ts: windows_terminal_start/send/read/list/kill (#35)
- tools/filesystem.ts: windows_file_read/write/edit, windows_search (#36-39)

Removes template API client/config/types (not needed for local OS MCP).

Authored-by: Moko Consulting
This commit is contained in:
Jonathan Miller
2026-05-25 21:03:40 -05:00
parent 36b642e23f
commit 03e7ea0e69
12 changed files with 1629 additions and 416 deletions
+247
View File
@@ -0,0 +1,247 @@
#!/usr/bin/env node
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { execFile, spawn, ChildProcess } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
export interface ShellResult {
stdout: string;
stderr: string;
exitCode: number;
}
const POWERSHELL = 'powershell.exe';
const DEFAULT_TIMEOUT = 30_000;
/**
* Run a PowerShell command and return stdout/stderr/exitCode.
* Commands are wrapped with -NoProfile -NonInteractive for speed and safety.
*/
export async function runPowerShell(
command: string,
options: { timeout?: number; cwd?: string } = {},
): Promise<ShellResult> {
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
try {
const { stdout, stderr } = await execFileAsync(POWERSHELL, [
'-NoProfile',
'-NonInteractive',
'-Command',
command,
], {
timeout,
cwd: options.cwd,
maxBuffer: 10 * 1024 * 1024,
windowsHide: true,
});
return { stdout: stdout.trimEnd(), stderr: stderr.trimEnd(), exitCode: 0 };
} catch (err: unknown) {
const e = err as { stdout?: string; stderr?: string; code?: number | string; killed?: boolean };
if (e.killed) {
return {
stdout: e.stdout?.trimEnd() ?? '',
stderr: `Command timed out after ${timeout}ms`,
exitCode: -1,
};
}
return {
stdout: e.stdout?.trimEnd() ?? '',
stderr: e.stderr?.trimEnd() ?? String(err),
exitCode: typeof e.code === 'number' ? e.code : 1,
};
}
}
/**
* Run a PowerShell command that returns JSON. Wraps output with ConvertTo-Json
* and parses the result.
*/
export async function runPowerShellJson<T = unknown>(
command: string,
options: { timeout?: number; cwd?: string } = {},
): Promise<T> {
const wrapped = `${command} | ConvertTo-Json -Depth 10 -Compress`;
const result = await runPowerShell(wrapped, options);
if (result.exitCode !== 0) {
throw new Error(result.stderr || `PowerShell exited with code ${result.exitCode}`);
}
if (!result.stdout) {
return [] as unknown as T;
}
return JSON.parse(result.stdout) as T;
}
/**
* Run a generic shell command (cmd, bash, pwsh).
*/
export async function runShell(
command: string,
options: {
shell?: 'pwsh' | 'cmd' | 'bash';
timeout?: number;
cwd?: string;
} = {},
): Promise<ShellResult> {
const shell = options.shell ?? 'pwsh';
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
let executable: string;
let args: string[];
switch (shell) {
case 'cmd':
executable = 'cmd.exe';
args = ['/c', command];
break;
case 'bash':
executable = 'bash';
args = ['-c', command];
break;
case 'pwsh':
default:
executable = POWERSHELL;
args = ['-NoProfile', '-NonInteractive', '-Command', command];
break;
}
try {
const { stdout, stderr } = await execFileAsync(executable, args, {
timeout,
cwd: options.cwd,
maxBuffer: 10 * 1024 * 1024,
windowsHide: true,
});
return { stdout: stdout.trimEnd(), stderr: stderr.trimEnd(), exitCode: 0 };
} catch (err: unknown) {
const e = err as { stdout?: string; stderr?: string; code?: number | string; killed?: boolean };
if (e.killed) {
return {
stdout: e.stdout?.trimEnd() ?? '',
stderr: `Command timed out after ${timeout}ms`,
exitCode: -1,
};
}
return {
stdout: e.stdout?.trimEnd() ?? '',
stderr: e.stderr?.trimEnd() ?? String(err),
exitCode: typeof e.code === 'number' ? e.code : 1,
};
}
}
// ── Persistent Terminal Sessions ─────────────────────────────────────────
export interface TerminalSession {
pid: number;
shell: string;
process: ChildProcess;
output: string[];
startedAt: Date;
}
const sessions = new Map<number, TerminalSession>();
const MAX_OUTPUT_LINES = 5000;
export function startSession(shell: 'pwsh' | 'cmd' | 'bash' | 'python' | 'node' | 'wsl' = 'pwsh'): TerminalSession {
let executable: string;
let args: string[];
switch (shell) {
case 'cmd':
executable = 'cmd.exe';
args = [];
break;
case 'bash':
executable = 'bash';
args = [];
break;
case 'python':
executable = 'python';
args = ['-i'];
break;
case 'node':
executable = 'node';
args = [];
break;
case 'wsl':
executable = 'wsl.exe';
args = [];
break;
case 'pwsh':
default:
executable = POWERSHELL;
args = ['-NoProfile', '-NoLogo'];
break;
}
const proc = spawn(executable, args, {
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
});
const session: TerminalSession = {
pid: proc.pid!,
shell,
process: proc,
output: [],
startedAt: new Date(),
};
const pushLine = (line: string) => {
session.output.push(line);
if (session.output.length > MAX_OUTPUT_LINES) {
session.output.splice(0, session.output.length - MAX_OUTPUT_LINES);
}
};
proc.stdout?.on('data', (data: Buffer) => {
data.toString().split('\n').forEach(pushLine);
});
proc.stderr?.on('data', (data: Buffer) => {
data.toString().split('\n').forEach(l => pushLine(`[stderr] ${l}`));
});
proc.on('exit', () => {
pushLine(`[session ended]`);
});
sessions.set(proc.pid!, session);
return session;
}
export function sendToSession(pid: number, input: string): void {
const session = sessions.get(pid);
if (!session) throw new Error(`No session with PID ${pid}`);
if (session.process.exitCode !== null) throw new Error(`Session ${pid} has ended`);
session.process.stdin?.write(input + '\n');
}
export function readSessionOutput(pid: number, offset = 0, length?: number): string[] {
const session = sessions.get(pid);
if (!session) throw new Error(`No session with PID ${pid}`);
const start = offset < 0 ? Math.max(0, session.output.length + offset) : offset;
const end = length !== undefined ? start + length : undefined;
return session.output.slice(start, end);
}
export function listSessions(): Array<{ pid: number; shell: string; running: boolean; lines: number; startedAt: string }> {
return Array.from(sessions.values()).map(s => ({
pid: s.pid,
shell: s.shell,
running: s.process.exitCode === null,
lines: s.output.length,
startedAt: s.startedAt.toISOString(),
}));
}
export function terminateSession(pid: number): boolean {
const session = sessions.get(pid);
if (!session) return false;
session.process.kill();
sessions.delete(pid);
return true;
}