@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
package/dist/shell.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
import { execFile, spawn } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const POWERSHELL = 'powershell.exe';
|
|
9
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
10
|
+
/**
|
|
11
|
+
* Run a PowerShell command and return stdout/stderr/exitCode.
|
|
12
|
+
* Commands are wrapped with -NoProfile -NonInteractive for speed and safety.
|
|
13
|
+
*/
|
|
14
|
+
export async function runPowerShell(command, options = {}) {
|
|
15
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
16
|
+
try {
|
|
17
|
+
const { stdout, stderr } = await execFileAsync(POWERSHELL, [
|
|
18
|
+
'-NoProfile',
|
|
19
|
+
'-NonInteractive',
|
|
20
|
+
'-Command',
|
|
21
|
+
command,
|
|
22
|
+
], {
|
|
23
|
+
timeout,
|
|
24
|
+
cwd: options.cwd,
|
|
25
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
26
|
+
windowsHide: true,
|
|
27
|
+
});
|
|
28
|
+
return { stdout: stdout.trimEnd(), stderr: stderr.trimEnd(), exitCode: 0 };
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const e = err;
|
|
32
|
+
if (e.killed) {
|
|
33
|
+
return {
|
|
34
|
+
stdout: e.stdout?.trimEnd() ?? '',
|
|
35
|
+
stderr: `Command timed out after ${timeout}ms`,
|
|
36
|
+
exitCode: -1,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
stdout: e.stdout?.trimEnd() ?? '',
|
|
41
|
+
stderr: e.stderr?.trimEnd() ?? String(err),
|
|
42
|
+
exitCode: typeof e.code === 'number' ? e.code : 1,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Run a PowerShell command that returns JSON. Wraps output with ConvertTo-Json
|
|
48
|
+
* and parses the result.
|
|
49
|
+
*/
|
|
50
|
+
export async function runPowerShellJson(command, options = {}) {
|
|
51
|
+
const wrapped = `${command} | ConvertTo-Json -Depth 10 -Compress`;
|
|
52
|
+
const result = await runPowerShell(wrapped, options);
|
|
53
|
+
if (result.exitCode !== 0) {
|
|
54
|
+
throw new Error(result.stderr || `PowerShell exited with code ${result.exitCode}`);
|
|
55
|
+
}
|
|
56
|
+
if (!result.stdout) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return JSON.parse(result.stdout);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run a generic shell command (cmd, bash, pwsh).
|
|
63
|
+
*/
|
|
64
|
+
export async function runShell(command, options = {}) {
|
|
65
|
+
const shell = options.shell ?? 'pwsh';
|
|
66
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
67
|
+
let executable;
|
|
68
|
+
let args;
|
|
69
|
+
switch (shell) {
|
|
70
|
+
case 'cmd':
|
|
71
|
+
executable = 'cmd.exe';
|
|
72
|
+
args = ['/c', command];
|
|
73
|
+
break;
|
|
74
|
+
case 'bash':
|
|
75
|
+
executable = 'bash';
|
|
76
|
+
args = ['-c', command];
|
|
77
|
+
break;
|
|
78
|
+
case 'pwsh':
|
|
79
|
+
default:
|
|
80
|
+
executable = POWERSHELL;
|
|
81
|
+
args = ['-NoProfile', '-NonInteractive', '-Command', command];
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const { stdout, stderr } = await execFileAsync(executable, args, {
|
|
86
|
+
timeout,
|
|
87
|
+
cwd: options.cwd,
|
|
88
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
89
|
+
windowsHide: true,
|
|
90
|
+
});
|
|
91
|
+
return { stdout: stdout.trimEnd(), stderr: stderr.trimEnd(), exitCode: 0 };
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const e = err;
|
|
95
|
+
if (e.killed) {
|
|
96
|
+
return {
|
|
97
|
+
stdout: e.stdout?.trimEnd() ?? '',
|
|
98
|
+
stderr: `Command timed out after ${timeout}ms`,
|
|
99
|
+
exitCode: -1,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
stdout: e.stdout?.trimEnd() ?? '',
|
|
104
|
+
stderr: e.stderr?.trimEnd() ?? String(err),
|
|
105
|
+
exitCode: typeof e.code === 'number' ? e.code : 1,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const sessions = new Map();
|
|
110
|
+
const MAX_OUTPUT_LINES = 5000;
|
|
111
|
+
export function startSession(shell = 'pwsh') {
|
|
112
|
+
let executable;
|
|
113
|
+
let args;
|
|
114
|
+
switch (shell) {
|
|
115
|
+
case 'cmd':
|
|
116
|
+
executable = 'cmd.exe';
|
|
117
|
+
args = [];
|
|
118
|
+
break;
|
|
119
|
+
case 'bash':
|
|
120
|
+
executable = 'bash';
|
|
121
|
+
args = [];
|
|
122
|
+
break;
|
|
123
|
+
case 'python':
|
|
124
|
+
executable = 'python';
|
|
125
|
+
args = ['-i'];
|
|
126
|
+
break;
|
|
127
|
+
case 'node':
|
|
128
|
+
executable = 'node';
|
|
129
|
+
args = [];
|
|
130
|
+
break;
|
|
131
|
+
case 'wsl':
|
|
132
|
+
executable = 'wsl.exe';
|
|
133
|
+
args = [];
|
|
134
|
+
break;
|
|
135
|
+
case 'pwsh':
|
|
136
|
+
default:
|
|
137
|
+
executable = POWERSHELL;
|
|
138
|
+
args = ['-NoProfile', '-NoLogo'];
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
const proc = spawn(executable, args, {
|
|
142
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
143
|
+
windowsHide: true,
|
|
144
|
+
detached: false,
|
|
145
|
+
});
|
|
146
|
+
// Don't let terminal sessions prevent the MCP server from exiting
|
|
147
|
+
proc.unref();
|
|
148
|
+
proc.stdout?.unref?.();
|
|
149
|
+
proc.stderr?.unref?.();
|
|
150
|
+
proc.stdin?.unref?.();
|
|
151
|
+
const session = {
|
|
152
|
+
pid: proc.pid,
|
|
153
|
+
shell,
|
|
154
|
+
process: proc,
|
|
155
|
+
output: [],
|
|
156
|
+
startedAt: new Date(),
|
|
157
|
+
};
|
|
158
|
+
const pushLine = (line) => {
|
|
159
|
+
session.output.push(line);
|
|
160
|
+
if (session.output.length > MAX_OUTPUT_LINES) {
|
|
161
|
+
session.output.splice(0, session.output.length - MAX_OUTPUT_LINES);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
proc.stdout?.on('data', (data) => {
|
|
165
|
+
data.toString().split('\n').forEach(pushLine);
|
|
166
|
+
});
|
|
167
|
+
proc.stderr?.on('data', (data) => {
|
|
168
|
+
data.toString().split('\n').forEach(l => pushLine(`[stderr] ${l}`));
|
|
169
|
+
});
|
|
170
|
+
proc.on('exit', () => {
|
|
171
|
+
pushLine(`[session ended]`);
|
|
172
|
+
});
|
|
173
|
+
sessions.set(proc.pid, session);
|
|
174
|
+
return session;
|
|
175
|
+
}
|
|
176
|
+
export function sendToSession(pid, input) {
|
|
177
|
+
const session = sessions.get(pid);
|
|
178
|
+
if (!session)
|
|
179
|
+
throw new Error(`No session with PID ${pid}`);
|
|
180
|
+
if (session.process.exitCode !== null)
|
|
181
|
+
throw new Error(`Session ${pid} has ended`);
|
|
182
|
+
session.process.stdin?.write(input + '\n');
|
|
183
|
+
}
|
|
184
|
+
export function readSessionOutput(pid, offset = 0, length) {
|
|
185
|
+
const session = sessions.get(pid);
|
|
186
|
+
if (!session)
|
|
187
|
+
throw new Error(`No session with PID ${pid}`);
|
|
188
|
+
const start = offset < 0 ? Math.max(0, session.output.length + offset) : offset;
|
|
189
|
+
const end = length !== undefined ? start + length : undefined;
|
|
190
|
+
return session.output.slice(start, end);
|
|
191
|
+
}
|
|
192
|
+
export function listSessions() {
|
|
193
|
+
return Array.from(sessions.values()).map(s => ({
|
|
194
|
+
pid: s.pid,
|
|
195
|
+
shell: s.shell,
|
|
196
|
+
running: s.process.exitCode === null,
|
|
197
|
+
lines: s.output.length,
|
|
198
|
+
startedAt: s.startedAt.toISOString(),
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
export function terminateSession(pid) {
|
|
202
|
+
const session = sessions.get(pid);
|
|
203
|
+
if (!session)
|
|
204
|
+
return false;
|
|
205
|
+
session.process.kill();
|
|
206
|
+
sessions.delete(pid);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tool: windows_installed_apps (#19)
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { runPowerShell } from '../shell.js';
|
|
8
|
+
export function registerAppsTools(server) {
|
|
9
|
+
server.tool('windows_installed_apps', 'List installed applications from registry and Microsoft Store.', {
|
|
10
|
+
filter: z.string().optional().describe('Filter by app name (substring)'),
|
|
11
|
+
sort: z.enum(['name', 'date', 'size']).default('name').describe('Sort order'),
|
|
12
|
+
limit: z.number().default(50).describe('Max results'),
|
|
13
|
+
}, async ({ filter, sort, limit }) => {
|
|
14
|
+
const filterClause = filter
|
|
15
|
+
? `| Where-Object { $_.DisplayName -like '*${filter.replace(/'/g, "''")}*' }`
|
|
16
|
+
: '';
|
|
17
|
+
const sortClause = sort === 'date'
|
|
18
|
+
? '| Sort-Object InstallDate -Descending'
|
|
19
|
+
: sort === 'size'
|
|
20
|
+
? '| Sort-Object { [int]$_.EstimatedSize } -Descending'
|
|
21
|
+
: '| Sort-Object DisplayName';
|
|
22
|
+
const ps = `
|
|
23
|
+
$regPaths = @(
|
|
24
|
+
'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
|
|
25
|
+
'HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
|
|
26
|
+
'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
$apps = $regPaths | ForEach-Object {
|
|
30
|
+
Get-ItemProperty -Path $_ -ErrorAction SilentlyContinue
|
|
31
|
+
} | Where-Object { $_.DisplayName } ${filterClause} ${sortClause} |
|
|
32
|
+
Select-Object -First ${limit} |
|
|
33
|
+
ForEach-Object {
|
|
34
|
+
[PSCustomObject]@{
|
|
35
|
+
Name = $_.DisplayName
|
|
36
|
+
Version = $_.DisplayVersion
|
|
37
|
+
Publisher = $_.Publisher
|
|
38
|
+
InstallDate = $_.InstallDate
|
|
39
|
+
SizeMB = if ($_.EstimatedSize) { [math]::Round($_.EstimatedSize / 1024, 1) } else { $null }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
$apps | ConvertTo-Json -Depth 3 -Compress`;
|
|
44
|
+
const result = await runPowerShell(ps, { timeout: 20000 });
|
|
45
|
+
if (result.exitCode !== 0) {
|
|
46
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
47
|
+
}
|
|
48
|
+
if (!result.stdout) {
|
|
49
|
+
return { content: [{ type: 'text', text: 'No apps found.' }] };
|
|
50
|
+
}
|
|
51
|
+
const apps = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
52
|
+
const lines = apps.map((a) => {
|
|
53
|
+
const size = a.SizeMB ? `${String(a.SizeMB).padStart(8)} MB` : ' ';
|
|
54
|
+
const date = a.InstallDate || ' ';
|
|
55
|
+
return `${(a.Name || '').padEnd(45).slice(0, 45)} ${(a.Version || '').padEnd(15).slice(0, 15)} ${date.padEnd(10)} ${size} ${(a.Publisher || '').slice(0, 25)}`;
|
|
56
|
+
});
|
|
57
|
+
const header = `${'Name'.padEnd(45)} ${'Version'.padEnd(15)} ${'Installed'.padEnd(10)} ${'Size'.padStart(11)} Publisher`;
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: 'text', text: `${header}\n${'─'.repeat(115)}\n${lines.join('\n')}\n\n${apps.length} applications` }],
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=apps.js.map
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
import { z } from 'zod';
|
|
8
|
+
import { runPowerShell } from '../shell.js';
|
|
9
|
+
const AUDIO_PS = `
|
|
10
|
+
Add-Type -TypeDefinition @'
|
|
11
|
+
using System;
|
|
12
|
+
using System.Runtime.InteropServices;
|
|
13
|
+
|
|
14
|
+
public class WinAudio {
|
|
15
|
+
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
16
|
+
interface IMMDeviceEnumerator {
|
|
17
|
+
int EnumAudioEndpoints(int dataFlow, int dwStateMask, out IntPtr ppDevices);
|
|
18
|
+
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice ppEndpoint);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
22
|
+
interface IMMDevice {
|
|
23
|
+
int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
27
|
+
interface IAudioEndpointVolume {
|
|
28
|
+
int RegisterControlChangeNotify(IntPtr pNotify);
|
|
29
|
+
int UnregisterControlChangeNotify(IntPtr pNotify);
|
|
30
|
+
int GetChannelCount(out int pnChannelCount);
|
|
31
|
+
int SetMasterVolumeLevel(float fLevelDB, ref Guid pguidEventContext);
|
|
32
|
+
int SetMasterVolumeLevelScalar(float fLevel, ref Guid pguidEventContext);
|
|
33
|
+
int GetMasterVolumeLevel(out float pfLevelDB);
|
|
34
|
+
int GetMasterVolumeLevelScalar(out float pfLevel);
|
|
35
|
+
int SetChannelVolumeLevel(int nChannel, float fLevelDB, ref Guid pguidEventContext);
|
|
36
|
+
int SetChannelVolumeLevelScalar(int nChannel, float fLevel, ref Guid pguidEventContext);
|
|
37
|
+
int GetChannelVolumeLevel(int nChannel, out float pfLevelDB);
|
|
38
|
+
int GetChannelVolumeLevelScalar(int nChannel, out float pfLevel);
|
|
39
|
+
int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, ref Guid pguidEventContext);
|
|
40
|
+
int GetMute([MarshalAs(UnmanagedType.Bool)] out bool pbMute);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private static IAudioEndpointVolume GetVolume() {
|
|
44
|
+
var type = Type.GetTypeFromCLSID(new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"));
|
|
45
|
+
var enumerator = (IMMDeviceEnumerator)Activator.CreateInstance(type);
|
|
46
|
+
IMMDevice device;
|
|
47
|
+
enumerator.GetDefaultAudioEndpoint(0, 1, out device);
|
|
48
|
+
var iid = new Guid("5CDF2C82-841E-4546-9722-0CF74078229A");
|
|
49
|
+
object obj;
|
|
50
|
+
device.Activate(ref iid, 0x17, IntPtr.Zero, out obj);
|
|
51
|
+
return (IAudioEndpointVolume)obj;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public static float GetVolumeLevel() {
|
|
55
|
+
var vol = GetVolume();
|
|
56
|
+
float level;
|
|
57
|
+
vol.GetMasterVolumeLevelScalar(out level);
|
|
58
|
+
return level;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public static bool GetMute() {
|
|
62
|
+
var vol = GetVolume();
|
|
63
|
+
bool muted;
|
|
64
|
+
vol.GetMute(out muted);
|
|
65
|
+
return muted;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public static void SetVolumeLevel(float level) {
|
|
69
|
+
var vol = GetVolume();
|
|
70
|
+
var ctx = Guid.Empty;
|
|
71
|
+
vol.SetMasterVolumeLevelScalar(level, ref ctx);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public static void SetMute(bool mute) {
|
|
75
|
+
var vol = GetVolume();
|
|
76
|
+
var ctx = Guid.Empty;
|
|
77
|
+
vol.SetMute(mute, ref ctx);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
'@ -ErrorAction Stop
|
|
81
|
+
`;
|
|
82
|
+
export function registerAudioTools(server) {
|
|
83
|
+
server.tool('windows_audio_get', 'Get current audio state: volume level (0-100), mute status, and default playback device.', {}, async () => {
|
|
84
|
+
const ps = `
|
|
85
|
+
${AUDIO_PS}
|
|
86
|
+
$volume = [math]::Round([WinAudio]::GetVolumeLevel() * 100)
|
|
87
|
+
$muted = [WinAudio]::GetMute()
|
|
88
|
+
$device = (Get-CimInstance Win32_SoundDevice | Where-Object { $_.Status -eq 'OK' } | Select-Object -First 1).Name
|
|
89
|
+
[PSCustomObject]@{
|
|
90
|
+
volume = $volume
|
|
91
|
+
muted = $muted
|
|
92
|
+
device = $device
|
|
93
|
+
} | ConvertTo-Json -Compress`;
|
|
94
|
+
const result = await runPowerShell(ps, { timeout: 60000 });
|
|
95
|
+
if (result.exitCode !== 0) {
|
|
96
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
97
|
+
}
|
|
98
|
+
const state = JSON.parse(result.stdout);
|
|
99
|
+
const muteIcon = state.muted ? '🔇' : (state.volume > 50 ? '🔊' : '🔉');
|
|
100
|
+
return {
|
|
101
|
+
content: [{
|
|
102
|
+
type: 'text',
|
|
103
|
+
text: `${muteIcon} Volume: ${state.volume}%${state.muted ? ' (MUTED)' : ''}\nDevice: ${state.device || 'Unknown'}`,
|
|
104
|
+
}],
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
server.tool('windows_audio_set', 'Set audio volume (0-100), mute/unmute, or toggle mute.', {
|
|
108
|
+
volume: z.number().min(0).max(100).optional().describe('Volume level 0-100'),
|
|
109
|
+
mute: z.enum(['true', 'false', 'toggle']).optional().describe('Mute state: true, false, or toggle'),
|
|
110
|
+
}, async ({ volume, mute }) => {
|
|
111
|
+
const commands = [AUDIO_PS];
|
|
112
|
+
if (volume !== undefined) {
|
|
113
|
+
commands.push(`[WinAudio]::SetVolumeLevel(${volume / 100})`);
|
|
114
|
+
}
|
|
115
|
+
if (mute === 'toggle') {
|
|
116
|
+
commands.push(`[WinAudio]::SetMute(-not [WinAudio]::GetMute())`);
|
|
117
|
+
}
|
|
118
|
+
else if (mute === 'true') {
|
|
119
|
+
commands.push(`[WinAudio]::SetMute($true)`);
|
|
120
|
+
}
|
|
121
|
+
else if (mute === 'false') {
|
|
122
|
+
commands.push(`[WinAudio]::SetMute($false)`);
|
|
123
|
+
}
|
|
124
|
+
// Read back state
|
|
125
|
+
commands.push(`
|
|
126
|
+
$vol = [math]::Round([WinAudio]::GetVolumeLevel() * 100)
|
|
127
|
+
$m = [WinAudio]::GetMute()
|
|
128
|
+
[PSCustomObject]@{ volume = $vol; muted = $m } | ConvertTo-Json -Compress`);
|
|
129
|
+
const result = await runPowerShell(commands.join('\n'));
|
|
130
|
+
if (result.exitCode !== 0) {
|
|
131
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
132
|
+
}
|
|
133
|
+
const state = JSON.parse(result.stdout);
|
|
134
|
+
return {
|
|
135
|
+
content: [{
|
|
136
|
+
type: 'text',
|
|
137
|
+
text: `Volume set to ${state.volume}%${state.muted ? ' (MUTED)' : ''}`,
|
|
138
|
+
}],
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=audio.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
import { z } from 'zod';
|
|
7
|
+
import { runPowerShell } from '../shell.js';
|
|
8
|
+
export function registerAudioAppTools(server) {
|
|
9
|
+
server.tool('windows_audio_app_volumes', 'Get or set per-application audio volume levels. Without set params, lists all app audio sessions.', {
|
|
10
|
+
app: z.string().optional().describe('App name to target (for set operations)'),
|
|
11
|
+
volume: z.number().min(0).max(100).optional().describe('Volume to set (0-100)'),
|
|
12
|
+
mute: z.enum(['true', 'false', 'toggle']).optional().describe('Mute state to set'),
|
|
13
|
+
}, async ({ app, volume, mute }) => {
|
|
14
|
+
// List mode — show all audio sessions via PowerShell + COM
|
|
15
|
+
const ps = `
|
|
16
|
+
Add-Type -TypeDefinition @'
|
|
17
|
+
using System;
|
|
18
|
+
using System.Runtime.InteropServices;
|
|
19
|
+
using System.Collections.Generic;
|
|
20
|
+
using System.Diagnostics;
|
|
21
|
+
|
|
22
|
+
public class AudioSessions {
|
|
23
|
+
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
24
|
+
interface IMMDeviceEnumerator {
|
|
25
|
+
int EnumAudioEndpoints(int dataFlow, int dwStateMask, out IntPtr ppDevices);
|
|
26
|
+
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice ppEndpoint);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
30
|
+
interface IMMDevice {
|
|
31
|
+
int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
[Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
35
|
+
interface IAudioSessionManager2 {
|
|
36
|
+
int _0(); // QueryInterface stuff
|
|
37
|
+
int _1();
|
|
38
|
+
int GetSessionEnumerator(out IAudioSessionEnumerator ppEnum);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
42
|
+
interface IAudioSessionEnumerator {
|
|
43
|
+
int GetCount(out int count);
|
|
44
|
+
int GetSession(int index, [MarshalAs(UnmanagedType.IUnknown)] out object ppSession);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public static string GetSessions() {
|
|
48
|
+
// For now, use a simpler approach via Get-Process
|
|
49
|
+
return "use_powershell";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
'@ -ErrorAction SilentlyContinue
|
|
53
|
+
|
|
54
|
+
# Use the Volume Mixer approach via Get-Process with audio
|
|
55
|
+
$sessions = Get-Process | Where-Object { $_.MainWindowTitle -ne '' -or $_.ProcessName -match 'chrome|firefox|spotify|vlc|teams|discord|zoom|music|video|media' } |
|
|
56
|
+
Select-Object Id, ProcessName, MainWindowTitle |
|
|
57
|
+
Sort-Object ProcessName -Unique
|
|
58
|
+
|
|
59
|
+
$sessions | ForEach-Object {
|
|
60
|
+
[PSCustomObject]@{
|
|
61
|
+
PID = $_.Id
|
|
62
|
+
Name = $_.ProcessName
|
|
63
|
+
Title = $_.MainWindowTitle
|
|
64
|
+
}
|
|
65
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
66
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
67
|
+
if (result.exitCode !== 0) {
|
|
68
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
69
|
+
}
|
|
70
|
+
if (!app && volume === undefined && !mute) {
|
|
71
|
+
// List mode
|
|
72
|
+
if (!result.stdout) {
|
|
73
|
+
return { content: [{ type: 'text', text: 'No audio app sessions detected.' }] };
|
|
74
|
+
}
|
|
75
|
+
const apps = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
76
|
+
const lines = apps.map((a) => `PID ${String(a.PID).padStart(6)} ${a.Name.padEnd(25)} ${a.Title || '(no window)'}`);
|
|
77
|
+
return {
|
|
78
|
+
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.` }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
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.` }],
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=audio_apps.js.map
|