327b51589a
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
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Universal: Changelog Validation / Validate CHANGELOG.md (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
MCP: Standards Compliance / Secret Scanning (push) Has been cancelled
MCP: Standards Compliance / License Header Validation (push) Has been cancelled
MCP: Build & Validate / build (20) (push) Has been cancelled
MCP: Build & Validate / build (22) (push) Has been cancelled
MCP: Standards Compliance / Repository Structure Validation (push) Has been cancelled
MCP: Standards Compliance / Coding Standards Check (push) Has been cancelled
Universal: Build & Release / Build & Release Pipeline (push) Has been cancelled
MCP: Standards Compliance / Workflow Configuration Check (push) Has been cancelled
MCP: Standards Compliance / Documentation Quality Check (push) Has been cancelled
MCP: Standards Compliance / README Completeness Check (push) Has been cancelled
MCP: Standards Compliance / Git Repository Hygiene (push) Has been cancelled
MCP: Standards Compliance / Line Length Check (push) Has been cancelled
MCP: Standards Compliance / File Naming Standards (push) Has been cancelled
MCP: Standards Compliance / Insecure Code Pattern Detection (push) Has been cancelled
MCP: Standards Compliance / Script Integrity Validation (push) Has been cancelled
MCP: Standards Compliance / Dead Code Detection (push) Has been cancelled
MCP: Standards Compliance / File Size Limits (push) Has been cancelled
MCP: Standards Compliance / Binary File Detection (push) Has been cancelled
MCP: Standards Compliance / TODO/FIXME Tracking (push) Has been cancelled
MCP: Build & Release / Build, Validate & Release (push) Has been cancelled
MCP: Standards Compliance / Broken Link Detection (push) Has been cancelled
MCP: Standards Compliance / API Documentation Coverage (push) Has been cancelled
MCP: Standards Compliance / Accessibility Check (push) Has been cancelled
MCP: Standards Compliance / Performance Metrics (push) Has been cancelled
MCP: Standards Compliance / Version Consistency Check (push) Has been cancelled
Universal: CodeQL Analysis / Analyze (actions) (push) Has been cancelled
MCP: Standards Compliance / Code Complexity Analysis (push) Has been cancelled
MCP: Standards Compliance / Code Duplication Detection (push) Has been cancelled
MCP: Standards Compliance / Unused Dependencies Check (push) Has been cancelled
MCP: Standards Compliance / Terraform Configuration Validation (push) Has been cancelled
MCP: Standards Compliance / Dependency Vulnerability Scanning (push) Has been cancelled
Universal: CodeQL Analysis / Analyze (javascript) (push) Has been cancelled
Universal: CodeQL Analysis / Security Scan Summary (push) Has been cancelled
MCP: Standards Compliance / Enterprise Readiness Check (push) Has been cancelled
MCP: Standards Compliance / Repository Health Check (push) Has been cancelled
MCP: Standards Compliance / Compliance Summary (push) Has been cancelled
Universal: Sync Version on Merge / Propagate README version (push) Has been cancelled
- audio.ts: increase Add-Type C# compilation timeout to 60s - system.ts: increase WMI query timeout to 45s - service.ts: batch WMI lookups (single query instead of per-service), 45s timeout - power.ts: increase powercfg timeout to 30s - window.ts: fix .Where() syntax to pipe-based Where-Object - shell.ts: unref terminal session child processes so MCP server can exit cleanly Test results: 36/36 passed (12 skipped — destructive/interactive) Authored-by: Moko Consulting
162 lines
5.6 KiB
TypeScript
162 lines
5.6 KiB
TypeScript
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* Tools: windows_audio_get (#6), windows_audio_set (#7)
|
|
* Uses compiled SetMute.exe for reliable COM audio control.
|
|
*/
|
|
|
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
import { z } from 'zod';
|
|
import { runPowerShell } from '../shell.js';
|
|
|
|
const AUDIO_PS = `
|
|
Add-Type -TypeDefinition @'
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
|
|
public class WinAudio {
|
|
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
interface IMMDeviceEnumerator {
|
|
int EnumAudioEndpoints(int dataFlow, int dwStateMask, out IntPtr ppDevices);
|
|
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice ppEndpoint);
|
|
}
|
|
|
|
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
interface IMMDevice {
|
|
int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
|
}
|
|
|
|
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
interface IAudioEndpointVolume {
|
|
int RegisterControlChangeNotify(IntPtr pNotify);
|
|
int UnregisterControlChangeNotify(IntPtr pNotify);
|
|
int GetChannelCount(out int pnChannelCount);
|
|
int SetMasterVolumeLevel(float fLevelDB, ref Guid pguidEventContext);
|
|
int SetMasterVolumeLevelScalar(float fLevel, ref Guid pguidEventContext);
|
|
int GetMasterVolumeLevel(out float pfLevelDB);
|
|
int GetMasterVolumeLevelScalar(out float pfLevel);
|
|
int SetChannelVolumeLevel(int nChannel, float fLevelDB, ref Guid pguidEventContext);
|
|
int SetChannelVolumeLevelScalar(int nChannel, float fLevel, ref Guid pguidEventContext);
|
|
int GetChannelVolumeLevel(int nChannel, out float pfLevelDB);
|
|
int GetChannelVolumeLevelScalar(int nChannel, out float pfLevel);
|
|
int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, ref Guid pguidEventContext);
|
|
int GetMute([MarshalAs(UnmanagedType.Bool)] out bool pbMute);
|
|
}
|
|
|
|
private static IAudioEndpointVolume GetVolume() {
|
|
var type = Type.GetTypeFromCLSID(new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"));
|
|
var enumerator = (IMMDeviceEnumerator)Activator.CreateInstance(type);
|
|
IMMDevice device;
|
|
enumerator.GetDefaultAudioEndpoint(0, 1, out device);
|
|
var iid = new Guid("5CDF2C82-841E-4546-9722-0CF74078229A");
|
|
object obj;
|
|
device.Activate(ref iid, 0x17, IntPtr.Zero, out obj);
|
|
return (IAudioEndpointVolume)obj;
|
|
}
|
|
|
|
public static float GetVolumeLevel() {
|
|
var vol = GetVolume();
|
|
float level;
|
|
vol.GetMasterVolumeLevelScalar(out level);
|
|
return level;
|
|
}
|
|
|
|
public static bool GetMute() {
|
|
var vol = GetVolume();
|
|
bool muted;
|
|
vol.GetMute(out muted);
|
|
return muted;
|
|
}
|
|
|
|
public static void SetVolumeLevel(float level) {
|
|
var vol = GetVolume();
|
|
var ctx = Guid.Empty;
|
|
vol.SetMasterVolumeLevelScalar(level, ref ctx);
|
|
}
|
|
|
|
public static void SetMute(bool mute) {
|
|
var vol = GetVolume();
|
|
var ctx = Guid.Empty;
|
|
vol.SetMute(mute, ref ctx);
|
|
}
|
|
}
|
|
'@ -ErrorAction Stop
|
|
`;
|
|
|
|
export function registerAudioTools(server: McpServer): void {
|
|
server.tool(
|
|
'windows_audio_get',
|
|
'Get current audio state: volume level (0-100), mute status, and default playback device.',
|
|
{},
|
|
async () => {
|
|
const ps = `
|
|
${AUDIO_PS}
|
|
$volume = [math]::Round([WinAudio]::GetVolumeLevel() * 100)
|
|
$muted = [WinAudio]::GetMute()
|
|
$device = (Get-CimInstance Win32_SoundDevice | Where-Object { $_.Status -eq 'OK' } | Select-Object -First 1).Name
|
|
[PSCustomObject]@{
|
|
volume = $volume
|
|
muted = $muted
|
|
device = $device
|
|
} | ConvertTo-Json -Compress`;
|
|
|
|
const result = await runPowerShell(ps, { timeout: 60000 });
|
|
if (result.exitCode !== 0) {
|
|
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
}
|
|
|
|
const state = JSON.parse(result.stdout);
|
|
const muteIcon = state.muted ? '🔇' : (state.volume > 50 ? '🔊' : '🔉');
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `${muteIcon} Volume: ${state.volume}%${state.muted ? ' (MUTED)' : ''}\nDevice: ${state.device || 'Unknown'}`,
|
|
}],
|
|
};
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
'windows_audio_set',
|
|
'Set audio volume (0-100), mute/unmute, or toggle mute.',
|
|
{
|
|
volume: z.number().min(0).max(100).optional().describe('Volume level 0-100'),
|
|
mute: z.enum(['true', 'false', 'toggle']).optional().describe('Mute state: true, false, or toggle'),
|
|
},
|
|
async ({ volume, mute }) => {
|
|
const commands: string[] = [AUDIO_PS];
|
|
|
|
if (volume !== undefined) {
|
|
commands.push(`[WinAudio]::SetVolumeLevel(${volume / 100})`);
|
|
}
|
|
|
|
if (mute === 'toggle') {
|
|
commands.push(`[WinAudio]::SetMute(-not [WinAudio]::GetMute())`);
|
|
} else if (mute === 'true') {
|
|
commands.push(`[WinAudio]::SetMute($true)`);
|
|
} else if (mute === 'false') {
|
|
commands.push(`[WinAudio]::SetMute($false)`);
|
|
}
|
|
|
|
// Read back state
|
|
commands.push(`
|
|
$vol = [math]::Round([WinAudio]::GetVolumeLevel() * 100)
|
|
$m = [WinAudio]::GetMute()
|
|
[PSCustomObject]@{ volume = $vol; muted = $m } | ConvertTo-Json -Compress`);
|
|
|
|
const result = await runPowerShell(commands.join('\n'));
|
|
if (result.exitCode !== 0) {
|
|
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
}
|
|
|
|
const state = JSON.parse(result.stdout);
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Volume set to ${state.volume}%${state.muted ? ' (MUTED)' : ''}`,
|
|
}],
|
|
};
|
|
},
|
|
);
|
|
}
|