@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,138 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_clipboard_get (#16), windows_clipboard_set (#17)
|
|
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 registerClipboardTools(server: McpServer): void {
|
|
12
|
+
server.tool(
|
|
13
|
+
'windows_clipboard_get',
|
|
14
|
+
'Read clipboard contents: text, file list, or image (returned as base64).',
|
|
15
|
+
{},
|
|
16
|
+
async () => {
|
|
17
|
+
// PowerShell must run in STA mode for clipboard access
|
|
18
|
+
const ps = `
|
|
19
|
+
powershell.exe -NoProfile -STA -Command {
|
|
20
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
21
|
+
$data = [System.Windows.Forms.Clipboard]::GetDataObject()
|
|
22
|
+
if (-not $data) { Write-Output '{"type":"empty","content":"Clipboard is empty"}'; return }
|
|
23
|
+
|
|
24
|
+
# Check for files
|
|
25
|
+
if ($data.ContainsFileDropList()) {
|
|
26
|
+
$files = [System.Windows.Forms.Clipboard]::GetFileDropList()
|
|
27
|
+
$list = @()
|
|
28
|
+
foreach ($f in $files) { $list += $f }
|
|
29
|
+
$json = @{ type = 'files'; content = $list } | ConvertTo-Json -Compress
|
|
30
|
+
Write-Output $json
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Check for image
|
|
35
|
+
if ($data.ContainsImage()) {
|
|
36
|
+
$img = [System.Windows.Forms.Clipboard]::GetImage()
|
|
37
|
+
$ms = New-Object System.IO.MemoryStream
|
|
38
|
+
$img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
39
|
+
$b64 = [Convert]::ToBase64String($ms.ToArray())
|
|
40
|
+
$ms.Dispose()
|
|
41
|
+
$img.Dispose()
|
|
42
|
+
$json = @{ type = 'image'; content = $b64 } | ConvertTo-Json -Compress
|
|
43
|
+
Write-Output $json
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Text
|
|
48
|
+
if ($data.ContainsText()) {
|
|
49
|
+
$text = [System.Windows.Forms.Clipboard]::GetText()
|
|
50
|
+
$json = @{ type = 'text'; content = $text } | ConvertTo-Json -Compress
|
|
51
|
+
Write-Output $json
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Write-Output '{"type":"unknown","content":"Clipboard contains unsupported format"}'
|
|
56
|
+
}`;
|
|
57
|
+
|
|
58
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
59
|
+
if (result.exitCode !== 0) {
|
|
60
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const data = JSON.parse(result.stdout);
|
|
65
|
+
|
|
66
|
+
if (data.type === 'image') {
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{ type: 'text' as const, text: 'Clipboard contains an image:' },
|
|
70
|
+
{ type: 'image' as const, data: data.content, mimeType: 'image/png' },
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (data.type === 'files') {
|
|
76
|
+
const files = Array.isArray(data.content) ? data.content : [data.content];
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: 'text', text: `Clipboard contains ${files.length} file(s):\n${files.join('\n')}` }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: 'text', text: data.content || '(empty)' }],
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
return { content: [{ type: 'text', text: result.stdout || '(empty clipboard)' }] };
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
server.tool(
|
|
92
|
+
'windows_clipboard_set',
|
|
93
|
+
'Set clipboard contents: text, file list, or clear.',
|
|
94
|
+
{
|
|
95
|
+
text: z.string().optional().describe('Text to copy to clipboard'),
|
|
96
|
+
files: z.array(z.string()).optional().describe('File paths to copy to clipboard'),
|
|
97
|
+
clear: z.boolean().optional().describe('Clear the clipboard'),
|
|
98
|
+
},
|
|
99
|
+
async ({ text, files, clear }) => {
|
|
100
|
+
if (clear) {
|
|
101
|
+
const result = await runPowerShell(`
|
|
102
|
+
powershell.exe -NoProfile -STA -Command {
|
|
103
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
104
|
+
[System.Windows.Forms.Clipboard]::Clear()
|
|
105
|
+
"Clipboard cleared"
|
|
106
|
+
}`);
|
|
107
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (text) {
|
|
111
|
+
// Escape for PowerShell here-string
|
|
112
|
+
const escaped = text.replace(/'/g, "''");
|
|
113
|
+
const result = await runPowerShell(`
|
|
114
|
+
powershell.exe -NoProfile -STA -Command {
|
|
115
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
116
|
+
[System.Windows.Forms.Clipboard]::SetText('${escaped}')
|
|
117
|
+
"Copied $([System.Windows.Forms.Clipboard]::GetText().Length) chars to clipboard"
|
|
118
|
+
}`);
|
|
119
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (files && files.length > 0) {
|
|
123
|
+
const pathList = files.map(f => `$fc.Add('${f.replace(/'/g, "''")}')`).join('; ');
|
|
124
|
+
const result = await runPowerShell(`
|
|
125
|
+
powershell.exe -NoProfile -STA -Command {
|
|
126
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
127
|
+
$fc = New-Object System.Collections.Specialized.StringCollection
|
|
128
|
+
${pathList}
|
|
129
|
+
[System.Windows.Forms.Clipboard]::SetFileDropList($fc)
|
|
130
|
+
"Copied $($fc.Count) file(s) to clipboard"
|
|
131
|
+
}`);
|
|
132
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { content: [{ type: 'text', text: 'Provide text, files, or clear.' }], isError: true };
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tool: windows_mcp_config (#40)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
|
|
13
|
+
const CONFIG_PATH = resolve(homedir(), '.mcp_windows.json');
|
|
14
|
+
|
|
15
|
+
interface McpConfig {
|
|
16
|
+
blockedCommands: string[];
|
|
17
|
+
allowedDirectories: string[];
|
|
18
|
+
outputLineLimit: number;
|
|
19
|
+
commandTimeout: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_CONFIG: McpConfig = {
|
|
23
|
+
blockedCommands: ['Format-Volume', 'Clear-Disk', 'Remove-Partition'],
|
|
24
|
+
allowedDirectories: [],
|
|
25
|
+
outputLineLimit: 5000,
|
|
26
|
+
commandTimeout: 30000,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let currentConfig: McpConfig | null = null;
|
|
30
|
+
|
|
31
|
+
async function loadConfig(): Promise<McpConfig> {
|
|
32
|
+
if (currentConfig) return currentConfig;
|
|
33
|
+
try {
|
|
34
|
+
const raw = await readFile(CONFIG_PATH, 'utf-8');
|
|
35
|
+
currentConfig = { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
36
|
+
} catch {
|
|
37
|
+
currentConfig = { ...DEFAULT_CONFIG };
|
|
38
|
+
}
|
|
39
|
+
return currentConfig!;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function saveConfig(config: McpConfig): Promise<void> {
|
|
43
|
+
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
44
|
+
currentConfig = config;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function registerConfigTools(server: McpServer): void {
|
|
48
|
+
server.tool(
|
|
49
|
+
'windows_mcp_config',
|
|
50
|
+
'Get or set mcp_windows configuration (blocked commands, allowed directories, output limits).',
|
|
51
|
+
{
|
|
52
|
+
action: z.enum(['get', 'set']).default('get').describe('Get or set config'),
|
|
53
|
+
key: z.string().optional().describe('Config key to set (blockedCommands, allowedDirectories, outputLineLimit, commandTimeout)'),
|
|
54
|
+
value: z.string().optional().describe('Value to set (JSON for arrays, number for limits)'),
|
|
55
|
+
},
|
|
56
|
+
async ({ action, key, value }) => {
|
|
57
|
+
const config = await loadConfig();
|
|
58
|
+
|
|
59
|
+
if (action === 'get') {
|
|
60
|
+
return {
|
|
61
|
+
content: [{
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: [
|
|
64
|
+
`mcp_windows configuration (${CONFIG_PATH}):`,
|
|
65
|
+
``,
|
|
66
|
+
`Blocked commands: ${config.blockedCommands.join(', ') || '(none)'}`,
|
|
67
|
+
`Allowed directories: ${config.allowedDirectories.length > 0 ? config.allowedDirectories.join(', ') : '(unrestricted)'}`,
|
|
68
|
+
`Output line limit: ${config.outputLineLimit}`,
|
|
69
|
+
`Command timeout: ${config.commandTimeout}ms`,
|
|
70
|
+
].join('\n'),
|
|
71
|
+
}],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!key || value === undefined) {
|
|
76
|
+
return { content: [{ type: 'text', text: 'Set requires key and value.' }], isError: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
switch (key) {
|
|
80
|
+
case 'blockedCommands':
|
|
81
|
+
case 'allowedDirectories':
|
|
82
|
+
try {
|
|
83
|
+
(config as unknown as Record<string, unknown>)[key] = JSON.parse(value);
|
|
84
|
+
} catch {
|
|
85
|
+
return { content: [{ type: 'text', text: `Value must be a JSON array (e.g. ["cmd1","cmd2"])` }], isError: true };
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case 'outputLineLimit':
|
|
89
|
+
case 'commandTimeout': {
|
|
90
|
+
const num = Number(value);
|
|
91
|
+
if (isNaN(num) || num < 0) {
|
|
92
|
+
return { content: [{ type: 'text', text: 'Value must be a positive number.' }], isError: true };
|
|
93
|
+
}
|
|
94
|
+
(config as unknown as Record<string, unknown>)[key] = num;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
return { content: [{ type: 'text', text: `Unknown key: ${key}. Valid: blockedCommands, allowedDirectories, outputLineLimit, commandTimeout` }], isError: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await saveConfig(config);
|
|
102
|
+
return { content: [{ type: 'text', text: `Set ${key} = ${value}` }] };
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tool: windows_dialog (#21)
|
|
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 registerDialogTools(server: McpServer): void {
|
|
12
|
+
server.tool(
|
|
13
|
+
'windows_dialog',
|
|
14
|
+
'Show system dialogs: message box, input prompt, file/folder picker.',
|
|
15
|
+
{
|
|
16
|
+
type: z.enum(['message', 'input', 'file_open', 'file_save', 'folder']).describe('Dialog type'),
|
|
17
|
+
title: z.string().default('mcp_windows').describe('Dialog title'),
|
|
18
|
+
message: z.string().optional().describe('Message text (for message/input)'),
|
|
19
|
+
buttons: z.enum(['ok', 'okcancel', 'yesno', 'yesnocancel']).default('ok').describe('Buttons (for message)'),
|
|
20
|
+
filter: z.string().optional().describe('File filter (for file dialogs, e.g. "Text files|*.txt|All files|*.*")'),
|
|
21
|
+
default_path: z.string().optional().describe('Default path/filename'),
|
|
22
|
+
},
|
|
23
|
+
async ({ type, title, message, buttons, filter, default_path }) => {
|
|
24
|
+
let ps: string;
|
|
25
|
+
|
|
26
|
+
switch (type) {
|
|
27
|
+
case 'message': {
|
|
28
|
+
const btnMap: Record<string, number> = { ok: 0, okcancel: 1, yesno: 4, yesnocancel: 3 };
|
|
29
|
+
ps = `
|
|
30
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
31
|
+
$result = [System.Windows.Forms.MessageBox]::Show('${(message || '').replace(/'/g, "''")}', '${title.replace(/'/g, "''")}', ${btnMap[buttons]})
|
|
32
|
+
$result.ToString()`;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
case 'input':
|
|
37
|
+
ps = `
|
|
38
|
+
Add-Type -AssemblyName Microsoft.VisualBasic
|
|
39
|
+
$result = [Microsoft.VisualBasic.Interaction]::InputBox('${(message || 'Enter value:').replace(/'/g, "''")}', '${title.replace(/'/g, "''")}', '${(default_path || '').replace(/'/g, "''")}')
|
|
40
|
+
if ($result) { $result } else { '__CANCELLED__' }`;
|
|
41
|
+
break;
|
|
42
|
+
|
|
43
|
+
case 'file_open':
|
|
44
|
+
ps = `
|
|
45
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
46
|
+
$dlg = New-Object System.Windows.Forms.OpenFileDialog
|
|
47
|
+
$dlg.Title = '${title.replace(/'/g, "''")}'
|
|
48
|
+
${filter ? `$dlg.Filter = '${filter.replace(/'/g, "''")}'` : ''}
|
|
49
|
+
${default_path ? `$dlg.InitialDirectory = '${default_path.replace(/'/g, "''")}'` : ''}
|
|
50
|
+
$dlg.Multiselect = $false
|
|
51
|
+
if ($dlg.ShowDialog() -eq 'OK') { $dlg.FileName } else { '__CANCELLED__' }`;
|
|
52
|
+
break;
|
|
53
|
+
|
|
54
|
+
case 'file_save':
|
|
55
|
+
ps = `
|
|
56
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
57
|
+
$dlg = New-Object System.Windows.Forms.SaveFileDialog
|
|
58
|
+
$dlg.Title = '${title.replace(/'/g, "''")}'
|
|
59
|
+
${filter ? `$dlg.Filter = '${filter.replace(/'/g, "''")}'` : ''}
|
|
60
|
+
${default_path ? `$dlg.FileName = '${default_path.replace(/'/g, "''")}'` : ''}
|
|
61
|
+
if ($dlg.ShowDialog() -eq 'OK') { $dlg.FileName } else { '__CANCELLED__' }`;
|
|
62
|
+
break;
|
|
63
|
+
|
|
64
|
+
case 'folder':
|
|
65
|
+
ps = `
|
|
66
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
67
|
+
$dlg = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
68
|
+
$dlg.Description = '${(message || title).replace(/'/g, "''")}'
|
|
69
|
+
${default_path ? `$dlg.SelectedPath = '${default_path.replace(/'/g, "''")}'` : ''}
|
|
70
|
+
if ($dlg.ShowDialog() -eq 'OK') { $dlg.SelectedPath } else { '__CANCELLED__' }`;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = await runPowerShell(ps, { timeout: 120000 }); // Long timeout — user interaction
|
|
75
|
+
if (result.exitCode !== 0) {
|
|
76
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const output = result.stdout.trim();
|
|
80
|
+
if (output === '__CANCELLED__') {
|
|
81
|
+
return { content: [{ type: 'text', text: 'Dialog cancelled by user.' }] };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { content: [{ type: 'text', text: output }] };
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_display_get (#9), windows_display_set (#10), windows_screenshot (#11)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { runPowerShell } from '../shell.js';
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
|
|
14
|
+
export function registerDisplayTools(server: McpServer): void {
|
|
15
|
+
server.tool(
|
|
16
|
+
'windows_display_get',
|
|
17
|
+
'Get display configuration: resolution, refresh rate, scaling, multi-monitor layout, HDR status.',
|
|
18
|
+
{},
|
|
19
|
+
async () => {
|
|
20
|
+
const ps = `
|
|
21
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
22
|
+
|
|
23
|
+
$screens = [System.Windows.Forms.Screen]::AllScreens
|
|
24
|
+
$monitors = Get-CimInstance -Namespace root\\wmi -ClassName WmiMonitorBasicDisplayParams -ErrorAction SilentlyContinue
|
|
25
|
+
$videoCtrl = Get-CimInstance Win32_VideoController
|
|
26
|
+
|
|
27
|
+
$i = 0
|
|
28
|
+
$screens | ForEach-Object {
|
|
29
|
+
$s = $_
|
|
30
|
+
$vc = $videoCtrl | Where-Object { $_.Name -match $s.DeviceName -or $true } | Select-Object -First 1
|
|
31
|
+
$dpiScale = [math]::Round(($s.Bounds.Width / $s.WorkingArea.Width) * 100, 0)
|
|
32
|
+
|
|
33
|
+
# Try to get real scaling from registry
|
|
34
|
+
$regScale = $null
|
|
35
|
+
try {
|
|
36
|
+
$regPath = "HKCU:\\Control Panel\\Desktop\\PerMonitorSettings"
|
|
37
|
+
if (Test-Path $regPath) {
|
|
38
|
+
$regScale = (Get-ChildItem $regPath -ErrorAction SilentlyContinue | Select-Object -Index $i | Get-ItemProperty -ErrorAction SilentlyContinue).DpiValue
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
|
|
42
|
+
[PSCustomObject]@{
|
|
43
|
+
Index = $i
|
|
44
|
+
Name = $s.DeviceName
|
|
45
|
+
Primary = $s.Primary
|
|
46
|
+
Resolution = "$($s.Bounds.Width)x$($s.Bounds.Height)"
|
|
47
|
+
RefreshRate = if ($vc) { "$($vc.CurrentRefreshRate) Hz" } else { 'Unknown' }
|
|
48
|
+
Scaling = if ($regScale) { "$($regScale)%" } else { 'System default' }
|
|
49
|
+
Position = "($($s.Bounds.X), $($s.Bounds.Y))"
|
|
50
|
+
WorkArea = "$($s.WorkingArea.Width)x$($s.WorkingArea.Height)"
|
|
51
|
+
BitsPerPixel = if ($vc) { $vc.CurrentBitsPerPixel } else { $null }
|
|
52
|
+
}
|
|
53
|
+
$i++
|
|
54
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
55
|
+
|
|
56
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
57
|
+
if (result.exitCode !== 0) {
|
|
58
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const displays = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
62
|
+
const lines = displays.map((d: { Index: number; Name: string; Primary: boolean; Resolution: string; RefreshRate: string; Scaling: string; Position: string; WorkArea: string }) =>
|
|
63
|
+
[
|
|
64
|
+
`Monitor ${d.Index}: ${d.Name}${d.Primary ? ' (Primary)' : ''}`,
|
|
65
|
+
` Resolution: ${d.Resolution} @ ${d.RefreshRate}`,
|
|
66
|
+
` Scaling: ${d.Scaling}`,
|
|
67
|
+
` Position: ${d.Position}`,
|
|
68
|
+
` Work area: ${d.WorkArea}`,
|
|
69
|
+
].join('\n'),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return { content: [{ type: 'text', text: lines.join('\n\n') }] };
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
server.tool(
|
|
77
|
+
'windows_display_set',
|
|
78
|
+
'Change display settings: resolution, brightness.',
|
|
79
|
+
{
|
|
80
|
+
resolution: z.string().optional().describe('Resolution as "WIDTHxHEIGHT" (e.g. "1920x1080")'),
|
|
81
|
+
brightness: z.number().min(0).max(100).optional().describe('Screen brightness 0-100 (laptops only)'),
|
|
82
|
+
},
|
|
83
|
+
async ({ resolution, brightness }) => {
|
|
84
|
+
const results: string[] = [];
|
|
85
|
+
|
|
86
|
+
if (brightness !== undefined) {
|
|
87
|
+
const ps = `
|
|
88
|
+
try {
|
|
89
|
+
$monitors = Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightnessMethods -ErrorAction Stop
|
|
90
|
+
$monitors | Invoke-CimMethod -MethodName WmiSetBrightness -Arguments @{Timeout=1; Brightness=${brightness}} -ErrorAction Stop
|
|
91
|
+
"Brightness set to ${brightness}%"
|
|
92
|
+
} catch {
|
|
93
|
+
"Error: Brightness control not available (requires laptop/integrated display). $_"
|
|
94
|
+
}`;
|
|
95
|
+
const r = await runPowerShell(ps);
|
|
96
|
+
results.push(r.stdout || r.stderr);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (resolution) {
|
|
100
|
+
const match = resolution.match(/^(\d+)x(\d+)$/);
|
|
101
|
+
if (!match) {
|
|
102
|
+
results.push('Invalid resolution format. Use WIDTHxHEIGHT (e.g. 1920x1080)');
|
|
103
|
+
} else {
|
|
104
|
+
const ps = `
|
|
105
|
+
Add-Type @'
|
|
106
|
+
using System;
|
|
107
|
+
using System.Runtime.InteropServices;
|
|
108
|
+
|
|
109
|
+
public class DisplaySettings {
|
|
110
|
+
[DllImport("user32.dll")]
|
|
111
|
+
public static extern int EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode);
|
|
112
|
+
[DllImport("user32.dll")]
|
|
113
|
+
public static extern int ChangeDisplaySettings(ref DEVMODE devMode, int flags);
|
|
114
|
+
|
|
115
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
|
116
|
+
public struct DEVMODE {
|
|
117
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
|
118
|
+
public string dmDeviceName;
|
|
119
|
+
public short dmSpecVersion;
|
|
120
|
+
public short dmDriverVersion;
|
|
121
|
+
public short dmSize;
|
|
122
|
+
public short dmDriverExtra;
|
|
123
|
+
public int dmFields;
|
|
124
|
+
public int dmPositionX;
|
|
125
|
+
public int dmPositionY;
|
|
126
|
+
public int dmDisplayOrientation;
|
|
127
|
+
public int dmDisplayFixedOutput;
|
|
128
|
+
public short dmColor;
|
|
129
|
+
public short dmDuplex;
|
|
130
|
+
public short dmYResolution;
|
|
131
|
+
public short dmTTOption;
|
|
132
|
+
public short dmCollate;
|
|
133
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
|
134
|
+
public string dmFormName;
|
|
135
|
+
public short dmLogPixels;
|
|
136
|
+
public int dmBitsPerPel;
|
|
137
|
+
public int dmPelsWidth;
|
|
138
|
+
public int dmPelsHeight;
|
|
139
|
+
public int dmDisplayFlags;
|
|
140
|
+
public int dmDisplayFrequency;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public static string SetResolution(int width, int height) {
|
|
144
|
+
DEVMODE dm = new DEVMODE();
|
|
145
|
+
dm.dmSize = (short)Marshal.SizeOf(typeof(DEVMODE));
|
|
146
|
+
if (EnumDisplaySettings(null, -1, ref dm) != 0) {
|
|
147
|
+
dm.dmPelsWidth = width;
|
|
148
|
+
dm.dmPelsHeight = height;
|
|
149
|
+
dm.dmFields = 0x80000 | 0x100000; // DM_PELSWIDTH | DM_PELSHEIGHT
|
|
150
|
+
int result = ChangeDisplaySettings(ref dm, 0);
|
|
151
|
+
if (result == 0) return "Resolution changed to " + width + "x" + height;
|
|
152
|
+
return "Failed to change resolution (code: " + result + "). Resolution may not be supported.";
|
|
153
|
+
}
|
|
154
|
+
return "Failed to enumerate display settings.";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
'@ -ErrorAction Stop
|
|
158
|
+
[DisplaySettings]::SetResolution(${match[1]}, ${match[2]})`;
|
|
159
|
+
const r = await runPowerShell(ps);
|
|
160
|
+
results.push(r.stdout || r.stderr);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (results.length === 0) {
|
|
165
|
+
return { content: [{ type: 'text', text: 'No changes specified. Provide resolution or brightness.' }], isError: true };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { content: [{ type: 'text', text: results.join('\n') }] };
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
server.tool(
|
|
173
|
+
'windows_screenshot',
|
|
174
|
+
'Capture a screenshot of the screen, a specific window, or a region. Returns as base64 image.',
|
|
175
|
+
{
|
|
176
|
+
target: z.enum(['screen', 'window', 'region']).default('screen').describe('What to capture'),
|
|
177
|
+
monitor: z.number().default(0).describe('Monitor index (for screen capture)'),
|
|
178
|
+
window_title: z.string().optional().describe('Window title substring (for window capture)'),
|
|
179
|
+
x: z.number().optional().describe('Region X (for region capture)'),
|
|
180
|
+
y: z.number().optional().describe('Region Y'),
|
|
181
|
+
width: z.number().optional().describe('Region width'),
|
|
182
|
+
height: z.number().optional().describe('Region height'),
|
|
183
|
+
save_path: z.string().optional().describe('Save to file instead of returning base64'),
|
|
184
|
+
},
|
|
185
|
+
async ({ target, monitor, window_title, x, y, width, height, save_path }) => {
|
|
186
|
+
const outPath = save_path ? resolve(save_path) : resolve(tmpdir(), `screenshot_${Date.now()}.png`);
|
|
187
|
+
|
|
188
|
+
let ps: string;
|
|
189
|
+
|
|
190
|
+
if (target === 'window' && window_title) {
|
|
191
|
+
ps = `
|
|
192
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
193
|
+
Add-Type -AssemblyName System.Drawing
|
|
194
|
+
Add-Type @'
|
|
195
|
+
using System;
|
|
196
|
+
using System.Runtime.InteropServices;
|
|
197
|
+
public class Win32Window {
|
|
198
|
+
[DllImport("user32.dll")] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
|
|
199
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
|
200
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
201
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
202
|
+
[DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count);
|
|
203
|
+
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
204
|
+
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
205
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
206
|
+
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
207
|
+
}
|
|
208
|
+
'@
|
|
209
|
+
$target = '${window_title.replace(/'/g, "''")}'
|
|
210
|
+
$found = $null
|
|
211
|
+
[Win32Window]::EnumWindows({
|
|
212
|
+
param($hWnd, $lParam)
|
|
213
|
+
$sb = New-Object System.Text.StringBuilder 256
|
|
214
|
+
[Win32Window]::GetWindowText($hWnd, $sb, 256) | Out-Null
|
|
215
|
+
$title = $sb.ToString()
|
|
216
|
+
if ($title -like "*$target*") { $script:found = $hWnd; return $false }
|
|
217
|
+
return $true
|
|
218
|
+
}, [IntPtr]::Zero) | Out-Null
|
|
219
|
+
|
|
220
|
+
if (-not $found) { throw "Window not found: $target" }
|
|
221
|
+
|
|
222
|
+
$rect = New-Object Win32Window+RECT
|
|
223
|
+
[Win32Window]::GetWindowRect($found, [ref]$rect) | Out-Null
|
|
224
|
+
$w = $rect.Right - $rect.Left
|
|
225
|
+
$h = $rect.Bottom - $rect.Top
|
|
226
|
+
$bmp = New-Object System.Drawing.Bitmap($w, $h)
|
|
227
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
228
|
+
$g.CopyFromScreen($rect.Left, $rect.Top, 0, 0, [System.Drawing.Size]::new($w, $h))
|
|
229
|
+
$g.Dispose()
|
|
230
|
+
$bmp.Save('${outPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
231
|
+
$bmp.Dispose()
|
|
232
|
+
"saved"`;
|
|
233
|
+
} else if (target === 'region' && x !== undefined && y !== undefined && width && height) {
|
|
234
|
+
ps = `
|
|
235
|
+
Add-Type -AssemblyName System.Drawing
|
|
236
|
+
$bmp = New-Object System.Drawing.Bitmap(${width}, ${height})
|
|
237
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
238
|
+
$g.CopyFromScreen(${x}, ${y}, 0, 0, [System.Drawing.Size]::new(${width}, ${height}))
|
|
239
|
+
$g.Dispose()
|
|
240
|
+
$bmp.Save('${outPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
241
|
+
$bmp.Dispose()
|
|
242
|
+
"saved"`;
|
|
243
|
+
} else {
|
|
244
|
+
// Full screen
|
|
245
|
+
ps = `
|
|
246
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
247
|
+
Add-Type -AssemblyName System.Drawing
|
|
248
|
+
$screen = [System.Windows.Forms.Screen]::AllScreens[${monitor}]
|
|
249
|
+
$bounds = $screen.Bounds
|
|
250
|
+
$bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
|
|
251
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
252
|
+
$g.CopyFromScreen($bounds.X, $bounds.Y, 0, 0, $bounds.Size)
|
|
253
|
+
$g.Dispose()
|
|
254
|
+
$bmp.Save('${outPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
255
|
+
$bmp.Dispose()
|
|
256
|
+
"saved"`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
260
|
+
if (result.exitCode !== 0) {
|
|
261
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (save_path) {
|
|
265
|
+
return { content: [{ type: 'text', text: `Screenshot saved: ${outPath}` }] };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Return as base64 image
|
|
269
|
+
try {
|
|
270
|
+
const data = await readFile(outPath);
|
|
271
|
+
// Clean up temp file
|
|
272
|
+
await import('node:fs/promises').then(fs => fs.unlink(outPath)).catch(() => {});
|
|
273
|
+
return {
|
|
274
|
+
content: [{
|
|
275
|
+
type: 'image' as const,
|
|
276
|
+
data: data.toString('base64'),
|
|
277
|
+
mimeType: 'image/png',
|
|
278
|
+
}],
|
|
279
|
+
};
|
|
280
|
+
} catch {
|
|
281
|
+
return { content: [{ type: 'text', text: `Screenshot captured at ${outPath} but failed to read back.` }] };
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
}
|