Files
mcp-windows/src/tools/audio.ts
T
Jonathan Miller 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
fix: resolve 6 test failures — timeouts and window_list syntax
- 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
2026-05-25 22:27:42 -05:00

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)' : ''}`,
}],
};
},
);
}