@mokoconsulting/mcp-windows 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +94 -0
- package/.gitmessage +9 -0
- package/.mokogitea/ISSUE_TEMPLATE/adr.md +110 -0
- package/.mokogitea/ISSUE_TEMPLATE/bug_report.md +48 -0
- package/.mokogitea/ISSUE_TEMPLATE/config.yml +18 -0
- package/.mokogitea/ISSUE_TEMPLATE/documentation.md +52 -0
- package/.mokogitea/ISSUE_TEMPLATE/feature_request.md +51 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_connection_issue.md +67 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_tool_request.md +49 -0
- package/.mokogitea/ISSUE_TEMPLATE/question.md +82 -0
- package/.mokogitea/ISSUE_TEMPLATE/rfc.md +126 -0
- package/.mokogitea/ISSUE_TEMPLATE/security.md +51 -0
- package/.mokogitea/ISSUE_TEMPLATE/version.md +24 -0
- package/.mokogitea/branch-protection.yml +251 -0
- package/.mokogitea/workflows/auto-assign.yml +76 -0
- package/.mokogitea/workflows/auto-bump.yml +66 -0
- package/.mokogitea/workflows/auto-dev-issue.yml +207 -0
- package/.mokogitea/workflows/auto-release.yml +421 -0
- package/.mokogitea/workflows/branch-cleanup.yml +48 -0
- package/.mokogitea/workflows/cascade-dev.yml +10 -0
- package/.mokogitea/workflows/changelog-validation.yml +101 -0
- package/.mokogitea/workflows/ci-generic.yml +191 -0
- package/.mokogitea/workflows/cleanup.yml +87 -0
- package/.mokogitea/workflows/codeql-analysis.yml +115 -0
- package/.mokogitea/workflows/copilot-agent.yml +44 -0
- package/.mokogitea/workflows/deploy-manual.yml +126 -0
- package/.mokogitea/workflows/enterprise-firewall-setup.yml +758 -0
- package/.mokogitea/workflows/gitleaks.yml +92 -0
- package/.mokogitea/workflows/issue-branch.yml +73 -0
- package/.mokogitea/workflows/mcp-auto-release.yml +278 -0
- package/.mokogitea/workflows/mcp-build-test.yml +65 -0
- package/.mokogitea/workflows/mcp-sdk-check.yml +109 -0
- package/.mokogitea/workflows/mcp-tool-inventory.yml +61 -0
- package/.mokogitea/workflows/notify.yml +70 -0
- package/.mokogitea/workflows/npm-publish.yml +113 -0
- package/.mokogitea/workflows/pr-check.yml +534 -0
- package/.mokogitea/workflows/pre-release.yml +252 -0
- package/.mokogitea/workflows/rc-revert.yml +66 -0
- package/.mokogitea/workflows/repo-health.yml +712 -0
- package/.mokogitea/workflows/repository-cleanup.yml +525 -0
- package/.mokogitea/workflows/security-audit.yml +82 -0
- package/.mokogitea/workflows/standards-compliance.yml +2614 -0
- package/.mokogitea/workflows/sync-version-on-merge.yml +133 -0
- package/.mokogitea/workflows/update-server.yml +312 -0
- package/.mokogitea/workflows/workflow-sync-trigger.yml +73 -0
- package/CHANGELOG.md +130 -0
- package/CLAUDE.md +49 -0
- package/CONTRIBUTING.md +161 -0
- package/ISSUES.md +601 -0
- package/Makefile +70 -0
- package/README.md +80 -0
- package/automation/ci-issue-reporter.sh +237 -0
- package/config.example.json +18 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +111 -0
- package/dist/shell.d.ts +50 -0
- package/dist/shell.js +209 -0
- package/dist/tools/apps.d.ts +3 -0
- package/dist/tools/apps.js +63 -0
- package/dist/tools/audio.d.ts +3 -0
- package/dist/tools/audio.js +142 -0
- package/dist/tools/audio_apps.d.ts +3 -0
- package/dist/tools/audio_apps.js +86 -0
- package/dist/tools/automation.d.ts +3 -0
- package/dist/tools/automation.js +261 -0
- package/dist/tools/bluetooth.d.ts +3 -0
- package/dist/tools/bluetooth.js +96 -0
- package/dist/tools/clipboard.d.ts +3 -0
- package/dist/tools/clipboard.js +118 -0
- package/dist/tools/config.d.ts +3 -0
- package/dist/tools/config.js +85 -0
- package/dist/tools/dialog.d.ts +3 -0
- package/dist/tools/dialog.js +72 -0
- package/dist/tools/display.d.ts +3 -0
- package/dist/tools/display.js +256 -0
- package/dist/tools/drives.d.ts +3 -0
- package/dist/tools/drives.js +98 -0
- package/dist/tools/environment.d.ts +3 -0
- package/dist/tools/environment.js +129 -0
- package/dist/tools/execute.d.ts +3 -0
- package/dist/tools/execute.js +28 -0
- package/dist/tools/filesystem.d.ts +3 -0
- package/dist/tools/filesystem.js +230 -0
- package/dist/tools/firewall.d.ts +3 -0
- package/dist/tools/firewall.js +108 -0
- package/dist/tools/hosts.d.ts +3 -0
- package/dist/tools/hosts.js +119 -0
- package/dist/tools/maintenance.d.ts +3 -0
- package/dist/tools/maintenance.js +236 -0
- package/dist/tools/netstat.d.ts +3 -0
- package/dist/tools/netstat.js +56 -0
- package/dist/tools/network.d.ts +3 -0
- package/dist/tools/network.js +70 -0
- package/dist/tools/notification.d.ts +3 -0
- package/dist/tools/notification.js +41 -0
- package/dist/tools/power.d.ts +3 -0
- package/dist/tools/power.js +104 -0
- package/dist/tools/printer.d.ts +3 -0
- package/dist/tools/printer.js +97 -0
- package/dist/tools/process.d.ts +3 -0
- package/dist/tools/process.js +54 -0
- package/dist/tools/process_kill.d.ts +3 -0
- package/dist/tools/process_kill.js +48 -0
- package/dist/tools/recycle_bin.d.ts +3 -0
- package/dist/tools/recycle_bin.js +108 -0
- package/dist/tools/registry.d.ts +3 -0
- package/dist/tools/registry.js +136 -0
- package/dist/tools/scheduler.d.ts +3 -0
- package/dist/tools/scheduler.js +116 -0
- package/dist/tools/service.d.ts +3 -0
- package/dist/tools/service.js +79 -0
- package/dist/tools/startup.d.ts +3 -0
- package/dist/tools/startup.js +159 -0
- package/dist/tools/storage.d.ts +3 -0
- package/dist/tools/storage.js +129 -0
- package/dist/tools/system.d.ts +3 -0
- package/dist/tools/system.js +84 -0
- package/dist/tools/system_mgmt.d.ts +3 -0
- package/dist/tools/system_mgmt.js +174 -0
- package/dist/tools/terminal.d.ts +3 -0
- package/dist/tools/terminal.js +80 -0
- package/dist/tools/theme.d.ts +3 -0
- package/dist/tools/theme.js +165 -0
- package/dist/tools/usb.d.ts +3 -0
- package/dist/tools/usb.js +52 -0
- package/dist/tools/virtual_desktop.d.ts +3 -0
- package/dist/tools/virtual_desktop.js +112 -0
- package/dist/tools/wifi.d.ts +3 -0
- package/dist/tools/wifi.js +136 -0
- package/dist/tools/window.d.ts +3 -0
- package/dist/tools/window.js +189 -0
- package/dist/tools/winget.d.ts +3 -0
- package/dist/tools/winget.js +79 -0
- package/dist/tools/wsl.d.ts +3 -0
- package/dist/tools/wsl.js +99 -0
- package/docs/API.md +63 -0
- package/docs/ARCHITECTURE.md +73 -0
- package/docs/INSTALLATION.md +102 -0
- package/docs/index.md +12 -0
- package/package.json +35 -0
- package/scripts/setup.mjs +123 -0
- package/src/index.ts +125 -0
- package/src/shell.ts +253 -0
- package/src/tools/apps.ts +76 -0
- package/src/tools/audio.ts +161 -0
- package/src/tools/audio_apps.ts +98 -0
- package/src/tools/automation.ts +297 -0
- package/src/tools/bluetooth.ts +114 -0
- package/src/tools/clipboard.ts +138 -0
- package/src/tools/config.ts +105 -0
- package/src/tools/dialog.ts +87 -0
- package/src/tools/display.ts +285 -0
- package/src/tools/drives.ts +124 -0
- package/src/tools/environment.ts +146 -0
- package/src/tools/execute.ts +35 -0
- package/src/tools/filesystem.ts +273 -0
- package/src/tools/firewall.ts +125 -0
- package/src/tools/hosts.ts +135 -0
- package/src/tools/maintenance.ts +299 -0
- package/src/tools/netstat.ts +72 -0
- package/src/tools/network.ts +84 -0
- package/src/tools/notification.ts +50 -0
- package/src/tools/power.ts +123 -0
- package/src/tools/printer.ts +114 -0
- package/src/tools/process.ts +80 -0
- package/src/tools/process_kill.ts +57 -0
- package/src/tools/recycle_bin.ts +126 -0
- package/src/tools/registry.ts +165 -0
- package/src/tools/scheduler.ts +140 -0
- package/src/tools/service.ts +102 -0
- package/src/tools/startup.ts +180 -0
- package/src/tools/storage.ts +141 -0
- package/src/tools/system.ts +99 -0
- package/src/tools/system_mgmt.ts +190 -0
- package/src/tools/terminal.ts +117 -0
- package/src/tools/theme.ts +205 -0
- package/src/tools/usb.ts +65 -0
- package/src/tools/virtual_desktop.ts +122 -0
- package/src/tools/wifi.ts +157 -0
- package/src/tools/window.ts +211 -0
- package/src/tools/winget.ts +100 -0
- package/src/tools/wsl.ts +112 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_audio_get (#6), windows_audio_set (#7)
|
|
5
|
+
* Uses compiled SetMute.exe for reliable COM audio control.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { runPowerShell } from '../shell.js';
|
|
11
|
+
|
|
12
|
+
const AUDIO_PS = `
|
|
13
|
+
Add-Type -TypeDefinition @'
|
|
14
|
+
using System;
|
|
15
|
+
using System.Runtime.InteropServices;
|
|
16
|
+
|
|
17
|
+
public class WinAudio {
|
|
18
|
+
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
19
|
+
interface IMMDeviceEnumerator {
|
|
20
|
+
int EnumAudioEndpoints(int dataFlow, int dwStateMask, out IntPtr ppDevices);
|
|
21
|
+
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice ppEndpoint);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
25
|
+
interface IMMDevice {
|
|
26
|
+
int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
30
|
+
interface IAudioEndpointVolume {
|
|
31
|
+
int RegisterControlChangeNotify(IntPtr pNotify);
|
|
32
|
+
int UnregisterControlChangeNotify(IntPtr pNotify);
|
|
33
|
+
int GetChannelCount(out int pnChannelCount);
|
|
34
|
+
int SetMasterVolumeLevel(float fLevelDB, ref Guid pguidEventContext);
|
|
35
|
+
int SetMasterVolumeLevelScalar(float fLevel, ref Guid pguidEventContext);
|
|
36
|
+
int GetMasterVolumeLevel(out float pfLevelDB);
|
|
37
|
+
int GetMasterVolumeLevelScalar(out float pfLevel);
|
|
38
|
+
int SetChannelVolumeLevel(int nChannel, float fLevelDB, ref Guid pguidEventContext);
|
|
39
|
+
int SetChannelVolumeLevelScalar(int nChannel, float fLevel, ref Guid pguidEventContext);
|
|
40
|
+
int GetChannelVolumeLevel(int nChannel, out float pfLevelDB);
|
|
41
|
+
int GetChannelVolumeLevelScalar(int nChannel, out float pfLevel);
|
|
42
|
+
int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, ref Guid pguidEventContext);
|
|
43
|
+
int GetMute([MarshalAs(UnmanagedType.Bool)] out bool pbMute);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private static IAudioEndpointVolume GetVolume() {
|
|
47
|
+
var type = Type.GetTypeFromCLSID(new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"));
|
|
48
|
+
var enumerator = (IMMDeviceEnumerator)Activator.CreateInstance(type);
|
|
49
|
+
IMMDevice device;
|
|
50
|
+
enumerator.GetDefaultAudioEndpoint(0, 1, out device);
|
|
51
|
+
var iid = new Guid("5CDF2C82-841E-4546-9722-0CF74078229A");
|
|
52
|
+
object obj;
|
|
53
|
+
device.Activate(ref iid, 0x17, IntPtr.Zero, out obj);
|
|
54
|
+
return (IAudioEndpointVolume)obj;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public static float GetVolumeLevel() {
|
|
58
|
+
var vol = GetVolume();
|
|
59
|
+
float level;
|
|
60
|
+
vol.GetMasterVolumeLevelScalar(out level);
|
|
61
|
+
return level;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public static bool GetMute() {
|
|
65
|
+
var vol = GetVolume();
|
|
66
|
+
bool muted;
|
|
67
|
+
vol.GetMute(out muted);
|
|
68
|
+
return muted;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public static void SetVolumeLevel(float level) {
|
|
72
|
+
var vol = GetVolume();
|
|
73
|
+
var ctx = Guid.Empty;
|
|
74
|
+
vol.SetMasterVolumeLevelScalar(level, ref ctx);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public static void SetMute(bool mute) {
|
|
78
|
+
var vol = GetVolume();
|
|
79
|
+
var ctx = Guid.Empty;
|
|
80
|
+
vol.SetMute(mute, ref ctx);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
'@ -ErrorAction Stop
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
export function registerAudioTools(server: McpServer): void {
|
|
87
|
+
server.tool(
|
|
88
|
+
'windows_audio_get',
|
|
89
|
+
'Get current audio state: volume level (0-100), mute status, and default playback device.',
|
|
90
|
+
{},
|
|
91
|
+
async () => {
|
|
92
|
+
const ps = `
|
|
93
|
+
${AUDIO_PS}
|
|
94
|
+
$volume = [math]::Round([WinAudio]::GetVolumeLevel() * 100)
|
|
95
|
+
$muted = [WinAudio]::GetMute()
|
|
96
|
+
$device = (Get-CimInstance Win32_SoundDevice | Where-Object { $_.Status -eq 'OK' } | Select-Object -First 1).Name
|
|
97
|
+
[PSCustomObject]@{
|
|
98
|
+
volume = $volume
|
|
99
|
+
muted = $muted
|
|
100
|
+
device = $device
|
|
101
|
+
} | ConvertTo-Json -Compress`;
|
|
102
|
+
|
|
103
|
+
const result = await runPowerShell(ps, { timeout: 60000 });
|
|
104
|
+
if (result.exitCode !== 0) {
|
|
105
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const state = JSON.parse(result.stdout);
|
|
109
|
+
const muteIcon = state.muted ? '🔇' : (state.volume > 50 ? '🔊' : '🔉');
|
|
110
|
+
return {
|
|
111
|
+
content: [{
|
|
112
|
+
type: 'text',
|
|
113
|
+
text: `${muteIcon} Volume: ${state.volume}%${state.muted ? ' (MUTED)' : ''}\nDevice: ${state.device || 'Unknown'}`,
|
|
114
|
+
}],
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
server.tool(
|
|
120
|
+
'windows_audio_set',
|
|
121
|
+
'Set audio volume (0-100), mute/unmute, or toggle mute.',
|
|
122
|
+
{
|
|
123
|
+
volume: z.number().min(0).max(100).optional().describe('Volume level 0-100'),
|
|
124
|
+
mute: z.enum(['true', 'false', 'toggle']).optional().describe('Mute state: true, false, or toggle'),
|
|
125
|
+
},
|
|
126
|
+
async ({ volume, mute }) => {
|
|
127
|
+
const commands: string[] = [AUDIO_PS];
|
|
128
|
+
|
|
129
|
+
if (volume !== undefined) {
|
|
130
|
+
commands.push(`[WinAudio]::SetVolumeLevel(${volume / 100})`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (mute === 'toggle') {
|
|
134
|
+
commands.push(`[WinAudio]::SetMute(-not [WinAudio]::GetMute())`);
|
|
135
|
+
} else if (mute === 'true') {
|
|
136
|
+
commands.push(`[WinAudio]::SetMute($true)`);
|
|
137
|
+
} else if (mute === 'false') {
|
|
138
|
+
commands.push(`[WinAudio]::SetMute($false)`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Read back state
|
|
142
|
+
commands.push(`
|
|
143
|
+
$vol = [math]::Round([WinAudio]::GetVolumeLevel() * 100)
|
|
144
|
+
$m = [WinAudio]::GetMute()
|
|
145
|
+
[PSCustomObject]@{ volume = $vol; muted = $m } | ConvertTo-Json -Compress`);
|
|
146
|
+
|
|
147
|
+
const result = await runPowerShell(commands.join('\n'));
|
|
148
|
+
if (result.exitCode !== 0) {
|
|
149
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const state = JSON.parse(result.stdout);
|
|
153
|
+
return {
|
|
154
|
+
content: [{
|
|
155
|
+
type: 'text',
|
|
156
|
+
text: `Volume set to ${state.volume}%${state.muted ? ' (MUTED)' : ''}`,
|
|
157
|
+
}],
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tool: windows_audio_app_volumes (#8)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { runPowerShell } from '../shell.js';
|
|
10
|
+
|
|
11
|
+
export function registerAudioAppTools(server: McpServer): void {
|
|
12
|
+
server.tool(
|
|
13
|
+
'windows_audio_app_volumes',
|
|
14
|
+
'Get or set per-application audio volume levels. Without set params, lists all app audio sessions.',
|
|
15
|
+
{
|
|
16
|
+
app: z.string().optional().describe('App name to target (for set operations)'),
|
|
17
|
+
volume: z.number().min(0).max(100).optional().describe('Volume to set (0-100)'),
|
|
18
|
+
mute: z.enum(['true', 'false', 'toggle']).optional().describe('Mute state to set'),
|
|
19
|
+
},
|
|
20
|
+
async ({ app, volume, mute }) => {
|
|
21
|
+
// List mode — show all audio sessions via PowerShell + COM
|
|
22
|
+
const ps = `
|
|
23
|
+
Add-Type -TypeDefinition @'
|
|
24
|
+
using System;
|
|
25
|
+
using System.Runtime.InteropServices;
|
|
26
|
+
using System.Collections.Generic;
|
|
27
|
+
using System.Diagnostics;
|
|
28
|
+
|
|
29
|
+
public class AudioSessions {
|
|
30
|
+
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
31
|
+
interface IMMDeviceEnumerator {
|
|
32
|
+
int EnumAudioEndpoints(int dataFlow, int dwStateMask, out IntPtr ppDevices);
|
|
33
|
+
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice ppEndpoint);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
37
|
+
interface IMMDevice {
|
|
38
|
+
int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
42
|
+
interface IAudioSessionManager2 {
|
|
43
|
+
int _0(); // QueryInterface stuff
|
|
44
|
+
int _1();
|
|
45
|
+
int GetSessionEnumerator(out IAudioSessionEnumerator ppEnum);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
[Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
49
|
+
interface IAudioSessionEnumerator {
|
|
50
|
+
int GetCount(out int count);
|
|
51
|
+
int GetSession(int index, [MarshalAs(UnmanagedType.IUnknown)] out object ppSession);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public static string GetSessions() {
|
|
55
|
+
// For now, use a simpler approach via Get-Process
|
|
56
|
+
return "use_powershell";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
'@ -ErrorAction SilentlyContinue
|
|
60
|
+
|
|
61
|
+
# Use the Volume Mixer approach via Get-Process with audio
|
|
62
|
+
$sessions = Get-Process | Where-Object { $_.MainWindowTitle -ne '' -or $_.ProcessName -match 'chrome|firefox|spotify|vlc|teams|discord|zoom|music|video|media' } |
|
|
63
|
+
Select-Object Id, ProcessName, MainWindowTitle |
|
|
64
|
+
Sort-Object ProcessName -Unique
|
|
65
|
+
|
|
66
|
+
$sessions | ForEach-Object {
|
|
67
|
+
[PSCustomObject]@{
|
|
68
|
+
PID = $_.Id
|
|
69
|
+
Name = $_.ProcessName
|
|
70
|
+
Title = $_.MainWindowTitle
|
|
71
|
+
}
|
|
72
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
73
|
+
|
|
74
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
75
|
+
if (result.exitCode !== 0) {
|
|
76
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!app && volume === undefined && !mute) {
|
|
80
|
+
// List mode
|
|
81
|
+
if (!result.stdout) {
|
|
82
|
+
return { content: [{ type: 'text', text: 'No audio app sessions detected.' }] };
|
|
83
|
+
}
|
|
84
|
+
const apps = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
85
|
+
const lines = apps.map((a: { PID: number; Name: string; Title: string }) =>
|
|
86
|
+
`PID ${String(a.PID).padStart(6)} ${a.Name.padEnd(25)} ${a.Title || '(no window)'}`,
|
|
87
|
+
);
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: 'text', text: `Audio-capable processes:\n\n${lines.join('\n')}\n\nNote: Per-app volume control requires the SndVol COM API. Use windows_audio_set for master volume.` }],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: 'text', text: `Per-app volume set/mute requires elevated SndVol COM access. Use windows_execute with PowerShell to control specific app audio, or use windows_audio_set for master volume.` }],
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_shortcut (#78), windows_input (#79),
|
|
5
|
+
* windows_font_list (#80), windows_sandbox (#81), windows_screen_capture (#82)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { runPowerShell } from '../shell.js';
|
|
11
|
+
|
|
12
|
+
export function registerAutomationTools(server: McpServer): void {
|
|
13
|
+
server.tool(
|
|
14
|
+
'windows_shortcut',
|
|
15
|
+
'Create, read, or modify Windows shortcut (.lnk) files.',
|
|
16
|
+
{
|
|
17
|
+
action: z.enum(['create', 'read']).describe('Action'),
|
|
18
|
+
path: z.string().describe('Shortcut .lnk file path'),
|
|
19
|
+
target: z.string().optional().describe('Target path (for create)'),
|
|
20
|
+
arguments: z.string().optional().describe('Arguments (for create)'),
|
|
21
|
+
icon: z.string().optional().describe('Icon path (for create)'),
|
|
22
|
+
working_dir: z.string().optional().describe('Working directory (for create)'),
|
|
23
|
+
description: z.string().optional().describe('Description (for create)'),
|
|
24
|
+
},
|
|
25
|
+
async ({ action, path, target, arguments: args, icon, working_dir, description }) => {
|
|
26
|
+
if (action === 'read') {
|
|
27
|
+
const ps = `
|
|
28
|
+
$ws = New-Object -ComObject WScript.Shell
|
|
29
|
+
$sc = $ws.CreateShortcut('${path.replace(/'/g, "''")}')
|
|
30
|
+
[PSCustomObject]@{
|
|
31
|
+
Target = $sc.TargetPath
|
|
32
|
+
Arguments = $sc.Arguments
|
|
33
|
+
WorkingDir = $sc.WorkingDirectory
|
|
34
|
+
Icon = $sc.IconLocation
|
|
35
|
+
Description = $sc.Description
|
|
36
|
+
Hotkey = $sc.Hotkey
|
|
37
|
+
WindowStyle = $sc.WindowStyle
|
|
38
|
+
} | ConvertTo-Json -Compress`;
|
|
39
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
40
|
+
if (result.exitCode !== 0) return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
41
|
+
const sc = JSON.parse(result.stdout);
|
|
42
|
+
return {
|
|
43
|
+
content: [{
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: `${path}\n Target: ${sc.Target}\n Args: ${sc.Arguments || '(none)'}\n Dir: ${sc.WorkingDir || '(none)'}\n Icon: ${sc.Icon || '(default)'}\n Description: ${sc.Description || '(none)'}`,
|
|
46
|
+
}],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!target) return { content: [{ type: 'text', text: 'create requires target.' }], isError: true };
|
|
51
|
+
const ps = `
|
|
52
|
+
$ws = New-Object -ComObject WScript.Shell
|
|
53
|
+
$sc = $ws.CreateShortcut('${path.replace(/'/g, "''")}')
|
|
54
|
+
$sc.TargetPath = '${target.replace(/'/g, "''")}'
|
|
55
|
+
${args ? `$sc.Arguments = '${args.replace(/'/g, "''")}'` : ''}
|
|
56
|
+
${working_dir ? `$sc.WorkingDirectory = '${working_dir.replace(/'/g, "''")}'` : ''}
|
|
57
|
+
${icon ? `$sc.IconLocation = '${icon.replace(/'/g, "''")}'` : ''}
|
|
58
|
+
${description ? `$sc.Description = '${description.replace(/'/g, "''")}'` : ''}
|
|
59
|
+
$sc.Save()
|
|
60
|
+
"Shortcut created: ${path}"`;
|
|
61
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
62
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }], isError: result.exitCode !== 0 };
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
server.tool(
|
|
67
|
+
'windows_input',
|
|
68
|
+
'Simulate keyboard presses, key combos, or type text. Also simulate mouse clicks and movement.',
|
|
69
|
+
{
|
|
70
|
+
type: z.enum(['key', 'combo', 'text', 'mouse_click', 'mouse_move']).describe('Input type'),
|
|
71
|
+
key: z.string().optional().describe('Key name for key/combo (e.g. "Enter", "Ctrl+C", "Alt+F4")'),
|
|
72
|
+
text: z.string().optional().describe('Text to type (for text type)'),
|
|
73
|
+
x: z.number().optional().describe('Mouse X coordinate'),
|
|
74
|
+
y: z.number().optional().describe('Mouse Y coordinate'),
|
|
75
|
+
button: z.enum(['left', 'right', 'middle']).default('left').describe('Mouse button'),
|
|
76
|
+
delay: z.number().default(50).describe('Delay between actions in ms'),
|
|
77
|
+
},
|
|
78
|
+
async ({ type, key, text, x, y, button, delay }) => {
|
|
79
|
+
let ps: string;
|
|
80
|
+
|
|
81
|
+
switch (type) {
|
|
82
|
+
case 'key':
|
|
83
|
+
if (!key) return { content: [{ type: 'text', text: 'key requires key name.' }], isError: true };
|
|
84
|
+
ps = `
|
|
85
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
86
|
+
[System.Windows.Forms.SendKeys]::SendWait('{${key.toUpperCase()}}')
|
|
87
|
+
"Sent key: ${key}"`;
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case 'combo':
|
|
91
|
+
if (!key) return { content: [{ type: 'text', text: 'combo requires key.' }], isError: true };
|
|
92
|
+
// Map Ctrl+C style to SendKeys format: ^c
|
|
93
|
+
const mapped = key
|
|
94
|
+
.replace(/Ctrl\+/gi, '^')
|
|
95
|
+
.replace(/Alt\+/gi, '%')
|
|
96
|
+
.replace(/Shift\+/gi, '+')
|
|
97
|
+
.replace(/Win\+/gi, '^{ESC}'); // approximate
|
|
98
|
+
ps = `
|
|
99
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
100
|
+
[System.Windows.Forms.SendKeys]::SendWait('${mapped}')
|
|
101
|
+
"Sent combo: ${key}"`;
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case 'text':
|
|
105
|
+
if (!text) return { content: [{ type: 'text', text: 'text requires text.' }], isError: true };
|
|
106
|
+
// Escape SendKeys special chars
|
|
107
|
+
const escaped = text.replace(/[+^%~(){}[\]]/g, '{$&}');
|
|
108
|
+
ps = `
|
|
109
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
110
|
+
[System.Windows.Forms.SendKeys]::SendWait('${escaped.replace(/'/g, "''")}')
|
|
111
|
+
"Typed ${text.length} characters"`;
|
|
112
|
+
break;
|
|
113
|
+
|
|
114
|
+
case 'mouse_click':
|
|
115
|
+
if (x === undefined || y === undefined) return { content: [{ type: 'text', text: 'mouse_click requires x and y.' }], isError: true };
|
|
116
|
+
const btnFlag = button === 'right' ? '0x0008,0x0010' : button === 'middle' ? '0x0020,0x0040' : '0x0002,0x0004';
|
|
117
|
+
ps = `
|
|
118
|
+
Add-Type @'
|
|
119
|
+
using System.Runtime.InteropServices;
|
|
120
|
+
public class MouseInput {
|
|
121
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
122
|
+
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, int dwExtraInfo);
|
|
123
|
+
}
|
|
124
|
+
'@
|
|
125
|
+
[MouseInput]::SetCursorPos(${x}, ${y})
|
|
126
|
+
Start-Sleep -Milliseconds ${delay}
|
|
127
|
+
[MouseInput]::mouse_event(${btnFlag.split(',')[0]}, 0, 0, 0, 0)
|
|
128
|
+
[MouseInput]::mouse_event(${btnFlag.split(',')[1]}, 0, 0, 0, 0)
|
|
129
|
+
"Clicked ${button} at (${x}, ${y})"`;
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case 'mouse_move':
|
|
133
|
+
if (x === undefined || y === undefined) return { content: [{ type: 'text', text: 'mouse_move requires x and y.' }], isError: true };
|
|
134
|
+
ps = `
|
|
135
|
+
Add-Type @'
|
|
136
|
+
using System.Runtime.InteropServices;
|
|
137
|
+
public class MouseMove { [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y); }
|
|
138
|
+
'@
|
|
139
|
+
[MouseMove]::SetCursorPos(${x}, ${y})
|
|
140
|
+
"Moved mouse to (${x}, ${y})"`;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
145
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }], isError: result.exitCode !== 0 };
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
server.tool(
|
|
150
|
+
'windows_font_list',
|
|
151
|
+
'List installed system and user fonts.',
|
|
152
|
+
{
|
|
153
|
+
filter: z.string().optional().describe('Filter by font name'),
|
|
154
|
+
},
|
|
155
|
+
async ({ filter }) => {
|
|
156
|
+
const filterClause = filter ? `| Where-Object { $_.Name -like '*${filter.replace(/'/g, "''")}*' }` : '';
|
|
157
|
+
const ps = `
|
|
158
|
+
Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts' |
|
|
159
|
+
ForEach-Object { $_.PSObject.Properties } |
|
|
160
|
+
Where-Object { $_.Name -notlike 'PS*' } |
|
|
161
|
+
ForEach-Object { [PSCustomObject]@{ Name = $_.Name; File = $_.Value } } ${filterClause} |
|
|
162
|
+
Sort-Object Name |
|
|
163
|
+
ConvertTo-Json -Depth 3 -Compress`;
|
|
164
|
+
|
|
165
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
166
|
+
if (result.exitCode !== 0) return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
167
|
+
if (!result.stdout) return { content: [{ type: 'text', text: 'No fonts found.' }] };
|
|
168
|
+
|
|
169
|
+
const fonts = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
170
|
+
const lines = fonts.map((f: { Name: string }) => ` ${f.Name}`);
|
|
171
|
+
return { content: [{ type: 'text', text: `Installed fonts (${fonts.length}):\n${lines.join('\n')}` }] };
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
server.tool(
|
|
176
|
+
'windows_sandbox',
|
|
177
|
+
'Check Windows Sandbox status, launch with default or custom config.',
|
|
178
|
+
{
|
|
179
|
+
action: z.enum(['status', 'launch', 'generate_config']).default('status').describe('Action'),
|
|
180
|
+
mapped_folder: z.string().optional().describe('Host folder to map into sandbox'),
|
|
181
|
+
read_only: z.boolean().default(true).describe('Map folder as read-only'),
|
|
182
|
+
networking: z.boolean().default(true).describe('Enable networking in sandbox'),
|
|
183
|
+
startup_command: z.string().optional().describe('Command to run on sandbox startup'),
|
|
184
|
+
config_path: z.string().optional().describe('Path to save/load .wsb config'),
|
|
185
|
+
},
|
|
186
|
+
async ({ action, mapped_folder, read_only, networking, startup_command, config_path }) => {
|
|
187
|
+
if (action === 'status') {
|
|
188
|
+
const ps = `
|
|
189
|
+
$feature = Get-WindowsOptionalFeature -Online -FeatureName 'Containers-DisposableClientVM' -ErrorAction SilentlyContinue
|
|
190
|
+
if ($feature) {
|
|
191
|
+
"Windows Sandbox: $($feature.State)"
|
|
192
|
+
} else {
|
|
193
|
+
"Windows Sandbox feature not found"
|
|
194
|
+
}`;
|
|
195
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
196
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (action === 'generate_config' || action === 'launch') {
|
|
200
|
+
const wsbLines = ['<Configuration>'];
|
|
201
|
+
if (!networking) wsbLines.push(' <Networking>Disable</Networking>');
|
|
202
|
+
if (mapped_folder) {
|
|
203
|
+
wsbLines.push(' <MappedFolders>');
|
|
204
|
+
wsbLines.push(' <MappedFolder>');
|
|
205
|
+
wsbLines.push(` <HostFolder>${mapped_folder}</HostFolder>`);
|
|
206
|
+
wsbLines.push(` <ReadOnly>${read_only ? 'true' : 'false'}</ReadOnly>`);
|
|
207
|
+
wsbLines.push(' </MappedFolder>');
|
|
208
|
+
wsbLines.push(' </MappedFolders>');
|
|
209
|
+
}
|
|
210
|
+
if (startup_command) {
|
|
211
|
+
wsbLines.push(` <LogonCommand><Command>${startup_command}</Command></LogonCommand>`);
|
|
212
|
+
}
|
|
213
|
+
wsbLines.push('</Configuration>');
|
|
214
|
+
const wsb = wsbLines.join('\n');
|
|
215
|
+
|
|
216
|
+
if (action === 'generate_config') {
|
|
217
|
+
if (config_path) {
|
|
218
|
+
const ps = `Set-Content -Path '${config_path.replace(/'/g, "''")}' -Value @'\n${wsb}\n'@\n"Config saved: ${config_path}"`;
|
|
219
|
+
const result = await runPowerShell(ps, { timeout: 5000 });
|
|
220
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
221
|
+
}
|
|
222
|
+
return { content: [{ type: 'text', text: wsb }] };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Launch
|
|
226
|
+
const tempWsb = config_path || `$env:TEMP\\mcp_sandbox_${Date.now()}.wsb`;
|
|
227
|
+
const ps = `
|
|
228
|
+
Set-Content -Path '${tempWsb}' -Value @'
|
|
229
|
+
${wsb}
|
|
230
|
+
'@
|
|
231
|
+
Start-Process '${tempWsb}'
|
|
232
|
+
"Sandbox launched${config_path ? '' : ' (temp config)'}"`;
|
|
233
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
234
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { content: [{ type: 'text', text: 'Unknown action.' }], isError: true };
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
server.tool(
|
|
242
|
+
'windows_screen_capture',
|
|
243
|
+
'Start or stop screen recording. Uses ffmpeg if available, otherwise reports status only.',
|
|
244
|
+
{
|
|
245
|
+
action: z.enum(['start', 'stop', 'status']).describe('Action'),
|
|
246
|
+
output: z.string().optional().describe('Output file path (for start)'),
|
|
247
|
+
fps: z.number().default(15).describe('Framerate'),
|
|
248
|
+
},
|
|
249
|
+
async ({ action, output, fps }) => {
|
|
250
|
+
if (action === 'status') {
|
|
251
|
+
const ps = `
|
|
252
|
+
$ffmpeg = Get-Command ffmpeg -ErrorAction SilentlyContinue
|
|
253
|
+
$recording = Get-Process ffmpeg -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like '*gdigrab*' }
|
|
254
|
+
[PSCustomObject]@{
|
|
255
|
+
FfmpegInstalled = $null -ne $ffmpeg
|
|
256
|
+
FfmpegPath = if ($ffmpeg) { $ffmpeg.Source } else { $null }
|
|
257
|
+
ActiveRecording = $null -ne $recording
|
|
258
|
+
RecordingPID = if ($recording) { $recording.Id } else { $null }
|
|
259
|
+
} | ConvertTo-Json -Compress`;
|
|
260
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
261
|
+
if (result.exitCode !== 0) return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
262
|
+
const info = JSON.parse(result.stdout);
|
|
263
|
+
return {
|
|
264
|
+
content: [{
|
|
265
|
+
type: 'text',
|
|
266
|
+
text: `ffmpeg: ${info.FfmpegInstalled ? info.FfmpegPath : 'Not installed (install via winget: winget install Gyan.FFmpeg)'}\nRecording: ${info.ActiveRecording ? `Active (PID ${info.RecordingPID})` : 'None'}`,
|
|
267
|
+
}],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (action === 'start') {
|
|
272
|
+
const outPath = output || `A:/temp/recording_${Date.now()}.mp4`;
|
|
273
|
+
const ps = `
|
|
274
|
+
$ffmpeg = Get-Command ffmpeg -ErrorAction SilentlyContinue
|
|
275
|
+
if (-not $ffmpeg) { throw "ffmpeg not installed. Install with: winget install Gyan.FFmpeg" }
|
|
276
|
+
Start-Process ffmpeg -ArgumentList '-f gdigrab -framerate ${fps} -i desktop -c:v libx264 -preset ultrafast "${outPath}"' -WindowStyle Hidden -PassThru | ForEach-Object { "Recording started (PID $($_.Id)). Output: ${outPath}" }`;
|
|
277
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
278
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }], isError: result.exitCode !== 0 };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (action === 'stop') {
|
|
282
|
+
const ps = `
|
|
283
|
+
$procs = Get-Process ffmpeg -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like '*gdigrab*' -or $_.CommandLine -eq $null }
|
|
284
|
+
if ($procs) {
|
|
285
|
+
$procs | ForEach-Object { $_.CloseMainWindow() | Out-Null }
|
|
286
|
+
Start-Sleep -Seconds 2
|
|
287
|
+
$procs | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
288
|
+
"Recording stopped ($(@($procs).Count) process(es))"
|
|
289
|
+
} else { "No active recording found" }`;
|
|
290
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
291
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { content: [{ type: 'text', text: 'Unknown action.' }], isError: true };
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_bluetooth_get (#45), windows_bluetooth_control (#46)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { runPowerShell } from '../shell.js';
|
|
10
|
+
|
|
11
|
+
export function registerBluetoothTools(server: McpServer): void {
|
|
12
|
+
server.tool(
|
|
13
|
+
'windows_bluetooth_get',
|
|
14
|
+
'Get Bluetooth adapter status and list paired/connected devices.',
|
|
15
|
+
{},
|
|
16
|
+
async () => {
|
|
17
|
+
const ps = `
|
|
18
|
+
$adapter = Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | Where-Object { $_.FriendlyName -match 'Bluetooth' -and $_.Class -eq 'Bluetooth' } | Select-Object -First 1
|
|
19
|
+
$enabled = if ($adapter) { $adapter.Status -eq 'OK' } else { $false }
|
|
20
|
+
|
|
21
|
+
$devices = Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue |
|
|
22
|
+
Where-Object { $_.FriendlyName -and $_.Class -eq 'Bluetooth' -and $_.FriendlyName -notmatch 'Bluetooth|Radio|Enumerator' } |
|
|
23
|
+
ForEach-Object {
|
|
24
|
+
[PSCustomObject]@{
|
|
25
|
+
Name = $_.FriendlyName
|
|
26
|
+
Status = $_.Status
|
|
27
|
+
InstanceId = $_.InstanceId
|
|
28
|
+
Connected = $_.Status -eq 'OK'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[PSCustomObject]@{
|
|
33
|
+
AdapterPresent = $null -ne $adapter
|
|
34
|
+
AdapterName = if ($adapter) { $adapter.FriendlyName } else { 'None' }
|
|
35
|
+
Enabled = $enabled
|
|
36
|
+
Devices = $devices
|
|
37
|
+
} | ConvertTo-Json -Depth 4 -Compress`;
|
|
38
|
+
|
|
39
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
40
|
+
if (result.exitCode !== 0) {
|
|
41
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const info = JSON.parse(result.stdout);
|
|
45
|
+
const lines: string[] = [
|
|
46
|
+
`Bluetooth: ${info.AdapterPresent ? (info.Enabled ? 'ON' : 'OFF') : 'Not available'}`,
|
|
47
|
+
`Adapter: ${info.AdapterName}`,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const devices = Array.isArray(info.Devices) ? info.Devices : info.Devices ? [info.Devices] : [];
|
|
51
|
+
if (devices.length > 0) {
|
|
52
|
+
lines.push('', 'Paired Devices:');
|
|
53
|
+
for (const d of devices) {
|
|
54
|
+
const icon = d.Connected ? '[CON]' : '[ ]';
|
|
55
|
+
lines.push(` ${icon} ${d.Name}`);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
lines.push('', 'No paired devices.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
server.tool(
|
|
66
|
+
'windows_bluetooth_control',
|
|
67
|
+
'Enable/disable Bluetooth adapter or disconnect a device.',
|
|
68
|
+
{
|
|
69
|
+
action: z.enum(['enable', 'disable', 'disconnect']).describe('Action'),
|
|
70
|
+
device: z.string().optional().describe('Device name (for disconnect)'),
|
|
71
|
+
},
|
|
72
|
+
async ({ action, device }) => {
|
|
73
|
+
let ps: string;
|
|
74
|
+
|
|
75
|
+
switch (action) {
|
|
76
|
+
case 'enable':
|
|
77
|
+
ps = `
|
|
78
|
+
$adapter = Get-PnpDevice -Class Bluetooth -ErrorAction Stop | Where-Object { $_.FriendlyName -match 'Bluetooth' -and $_.Class -eq 'Bluetooth' } | Select-Object -First 1
|
|
79
|
+
if ($adapter) {
|
|
80
|
+
Enable-PnpDevice -InstanceId $adapter.InstanceId -Confirm:$false -ErrorAction Stop
|
|
81
|
+
"Bluetooth enabled"
|
|
82
|
+
} else { "No Bluetooth adapter found" }`;
|
|
83
|
+
break;
|
|
84
|
+
case 'disable':
|
|
85
|
+
ps = `
|
|
86
|
+
$adapter = Get-PnpDevice -Class Bluetooth -ErrorAction Stop | Where-Object { $_.FriendlyName -match 'Bluetooth' -and $_.Class -eq 'Bluetooth' } | Select-Object -First 1
|
|
87
|
+
if ($adapter) {
|
|
88
|
+
Disable-PnpDevice -InstanceId $adapter.InstanceId -Confirm:$false -ErrorAction Stop
|
|
89
|
+
"Bluetooth disabled"
|
|
90
|
+
} else { "No Bluetooth adapter found" }`;
|
|
91
|
+
break;
|
|
92
|
+
case 'disconnect':
|
|
93
|
+
if (!device) {
|
|
94
|
+
return { content: [{ type: 'text', text: 'Disconnect requires device name.' }], isError: true };
|
|
95
|
+
}
|
|
96
|
+
ps = `
|
|
97
|
+
$dev = Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | Where-Object { $_.FriendlyName -like '*${device.replace(/'/g, "''")}*' } | Select-Object -First 1
|
|
98
|
+
if ($dev) {
|
|
99
|
+
Disable-PnpDevice -InstanceId $dev.InstanceId -Confirm:$false -ErrorAction Stop
|
|
100
|
+
Start-Sleep -Seconds 1
|
|
101
|
+
Enable-PnpDevice -InstanceId $dev.InstanceId -Confirm:$false -ErrorAction Stop
|
|
102
|
+
"Disconnected and re-enabled: $($dev.FriendlyName)"
|
|
103
|
+
} else { "Device not found: ${device}" }`;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: 'text', text: result.stdout || result.stderr }],
|
|
110
|
+
isError: result.exitCode !== 0,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
}
|