@harusame64/desktop-touch-mcp 0.15.4 → 0.15.5
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/README.ja.md +9 -3
- package/README.md +9 -3
- package/bin/launcher.js +109 -31
- package/package.json +3 -2
package/README.ja.md
CHANGED
|
@@ -10,7 +10,7 @@ Claude がデスクトップを直接見て、直接操作する。
|
|
|
10
10
|
マウス・キーボード・スクリーンショット・Windows UI Automation・Chrome DevTools Protocol・ターミナル・SmartScroll・Reactive Perception Graph を統合した 57 のツールを提供する MCP サーバーです。
|
|
11
11
|
|
|
12
12
|
> *v0.15: Rust ネイティブエンジンにより**平均 82 倍高速化** — UIA フォーカス取得 2ms、SSE2 SIMD 画像差分 13〜15 倍速。設定不要:エンジンは自動ロード、不在時は PowerShell に透過フォールバック。*
|
|
13
|
-
> *v0.15.
|
|
13
|
+
> *v0.15.5: **固定リリース検証** — npm ランチャーは対応する GitHub Release tag だけを取得し、Windows runtime zip を検証してから展開します。*
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -51,7 +51,9 @@ Claude がデスクトップを直接見て、直接操作する。
|
|
|
51
51
|
npx -y @harusame64/desktop-touch-mcp
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
npm
|
|
54
|
+
npm ランチャーは npm package version に厳密に対応する runtime だけを取得します。`X.Y.Z` を実行した場合は GitHub Release `vX.Y.Z` のみを参照し、`desktop-touch-mcp-windows.zip` をダウンロードして SHA256 を検証できた場合にだけ `%USERPROFILE%\.desktop-touch-mcp` へ展開します。検証済みキャッシュは次回以降も再利用されます。
|
|
55
|
+
|
|
56
|
+
キャッシュの保存先は `DESKTOP_TOUCH_MCP_HOME` で変更できます。
|
|
55
57
|
|
|
56
58
|
### Claude CLI への登録
|
|
57
59
|
|
|
@@ -92,7 +94,11 @@ cd desktop-touch-mcp
|
|
|
92
94
|
npm install
|
|
93
95
|
```
|
|
94
96
|
|
|
95
|
-
`npm install`
|
|
97
|
+
`npm install` 後にビルドを実行してください。
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm run build
|
|
101
|
+
```
|
|
96
102
|
|
|
97
103
|
ローカルチェックアウトを使う場合は、ビルド済みのサーバーを直接登録します。
|
|
98
104
|
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
An MCP server that gives Claude eyes and hands on Windows — 57 tools covering screenshots, mouse, keyboard, Windows UI Automation, Chrome DevTools Protocol, clipboard, desktop notifications, SmartScroll, and a Reactive Perception Graph for safe multi-step automation, designed from the ground up for LLM efficiency.
|
|
10
10
|
|
|
11
11
|
> *v0.15: **82× average speedup** via Rust native engine — UIA focus queries in 2 ms, SSE2-accelerated image diffing at 13–15× native speed. Zero-config: the engine auto-loads when present, with transparent PowerShell fallback.*
|
|
12
|
-
> *v0.15.
|
|
12
|
+
> *v0.15.5: **Pinned release verification** — the npm launcher now fetches only the matching GitHub Release tag and verifies the Windows runtime zip before extraction.*
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -50,7 +50,9 @@ An MCP server that gives Claude eyes and hands on Windows — 57 tools covering
|
|
|
50
50
|
npx -y @harusame64/desktop-touch-mcp
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
The npm launcher
|
|
53
|
+
The npm launcher resolves runtime strictly by npm package version. For package `X.Y.Z`, it fetches only GitHub Release tag `vX.Y.Z`, downloads `desktop-touch-mcp-windows.zip`, verifies its SHA256 digest, and only then expands it under `%USERPROFILE%\.desktop-touch-mcp`. Verified cached releases are reused on later runs.
|
|
54
|
+
|
|
55
|
+
Set `DESKTOP_TOUCH_MCP_HOME` to override the cache root directory.
|
|
54
56
|
|
|
55
57
|
### Register with Claude CLI
|
|
56
58
|
|
|
@@ -92,7 +94,11 @@ cd desktop-touch-mcp
|
|
|
92
94
|
npm install
|
|
93
95
|
```
|
|
94
96
|
|
|
95
|
-
|
|
97
|
+
Build after install:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm run build
|
|
101
|
+
```
|
|
96
102
|
|
|
97
103
|
For a local checkout, register the built server directly:
|
|
98
104
|
|
package/bin/launcher.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execFile, spawn } from "node:child_process";
|
|
4
|
-
import { createWriteStream, existsSync } from "node:fs";
|
|
4
|
+
import { createReadStream, createWriteStream, existsSync } from "node:fs";
|
|
5
5
|
import {
|
|
6
6
|
mkdir,
|
|
7
7
|
mkdtemp,
|
|
@@ -11,13 +11,22 @@ import {
|
|
|
11
11
|
rm,
|
|
12
12
|
writeFile,
|
|
13
13
|
} from "node:fs/promises";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
14
15
|
import os from "node:os";
|
|
15
16
|
import path from "node:path";
|
|
16
17
|
import { Readable } from "node:stream";
|
|
17
18
|
import { pipeline } from "node:stream/promises";
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
+
const PACKAGE_VERSION = "0.15.5";
|
|
21
|
+
const RELEASE_TAG = `v${PACKAGE_VERSION}`;
|
|
22
|
+
const REPO_API_URL = `https://api.github.com/repos/Harusame64/desktop-touch-mcp/releases/tags/${RELEASE_TAG}`;
|
|
20
23
|
const ASSET_NAME = "desktop-touch-mcp-windows.zip";
|
|
24
|
+
const RELEASE_METADATA_FILE = ".desktop-touch-release.json";
|
|
25
|
+
const RELEASE_MANIFEST = {
|
|
26
|
+
tagName: "v0.15.5",
|
|
27
|
+
assetName: ASSET_NAME,
|
|
28
|
+
sha256: "0150c5b1b2bfda9b39f2401bab49bf9fd02e5db1f3915300490ea7a93603f402",
|
|
29
|
+
};
|
|
21
30
|
const CACHE_ROOT = process.env.DESKTOP_TOUCH_MCP_HOME
|
|
22
31
|
? path.resolve(process.env.DESKTOP_TOUCH_MCP_HOME)
|
|
23
32
|
: path.join(os.homedir(), ".desktop-touch-mcp");
|
|
@@ -42,33 +51,83 @@ function releaseDirForTag(tagName) {
|
|
|
42
51
|
return path.join(RELEASES_DIR, tagToDirName(tagName));
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
return
|
|
54
|
+
function releaseMetadataPath(releaseDir) {
|
|
55
|
+
return path.join(releaseDir, RELEASE_METADATA_FILE);
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
function expectedReleaseSpec() {
|
|
59
|
+
if (RELEASE_MANIFEST.tagName !== RELEASE_TAG) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Release manifest mismatch: PACKAGE_VERSION=${PACKAGE_VERSION}, manifest=${RELEASE_MANIFEST.tagName}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (!RELEASE_MANIFEST.sha256 || RELEASE_MANIFEST.assetName !== ASSET_NAME) {
|
|
65
|
+
throw new Error(`Missing release manifest for ${RELEASE_TAG}`);
|
|
66
|
+
}
|
|
67
|
+
if (!/^[a-f0-9]{64}$/i.test(RELEASE_MANIFEST.sha256)) {
|
|
68
|
+
throw new Error(`Invalid release SHA256 manifest for ${RELEASE_TAG}`);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
tagName: RELEASE_MANIFEST.tagName,
|
|
72
|
+
assetName: RELEASE_MANIFEST.assetName,
|
|
73
|
+
sha256: String(RELEASE_MANIFEST.sha256).toLowerCase(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function readReleaseMetadata(releaseDir) {
|
|
78
|
+
try {
|
|
79
|
+
const raw = await readFile(releaseMetadataPath(releaseDir), "utf8");
|
|
80
|
+
return JSON.parse(raw);
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function isInstalled(releaseDir, expected) {
|
|
87
|
+
if (!existsSync(path.join(releaseDir, "dist", "index.js"))) return false;
|
|
88
|
+
const metadata = await readReleaseMetadata(releaseDir);
|
|
89
|
+
if (!metadata) return false;
|
|
90
|
+
return (
|
|
91
|
+
metadata.tagName === expected.tagName &&
|
|
92
|
+
metadata.assetName === expected.assetName &&
|
|
93
|
+
String(metadata.sha256 || "").toLowerCase() === expected.sha256
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function readCurrentRelease(expected) {
|
|
50
98
|
try {
|
|
51
99
|
const raw = await readFile(CURRENT_FILE, "utf8");
|
|
52
100
|
const parsed = JSON.parse(raw);
|
|
53
101
|
if (typeof parsed?.tagName !== "string") return null;
|
|
102
|
+
if (parsed.tagName !== expected.tagName) return null;
|
|
103
|
+
if (parsed.assetName !== expected.assetName) return null;
|
|
104
|
+
if (String(parsed.sha256 || "").toLowerCase() !== expected.sha256) return null;
|
|
54
105
|
const releaseDir = releaseDirForTag(parsed.tagName);
|
|
55
|
-
if (!(await isInstalled(releaseDir))) return null;
|
|
106
|
+
if (!(await isInstalled(releaseDir, expected))) return null;
|
|
56
107
|
return { tagName: parsed.tagName, releaseDir };
|
|
57
108
|
} catch {
|
|
58
109
|
return null;
|
|
59
110
|
}
|
|
60
111
|
}
|
|
61
112
|
|
|
62
|
-
async function
|
|
113
|
+
async function writeReleaseMetadata(releaseDir, expected) {
|
|
114
|
+
await writeFile(
|
|
115
|
+
releaseMetadataPath(releaseDir),
|
|
116
|
+
`${JSON.stringify({ ...expected, updatedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
117
|
+
"utf8"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function writeCurrentRelease(expected) {
|
|
63
122
|
await mkdir(CACHE_ROOT, { recursive: true });
|
|
64
123
|
await writeFile(
|
|
65
124
|
CURRENT_FILE,
|
|
66
|
-
`${JSON.stringify({
|
|
125
|
+
`${JSON.stringify({ ...expected, updatedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
67
126
|
"utf8"
|
|
68
127
|
);
|
|
69
128
|
}
|
|
70
129
|
|
|
71
|
-
async function
|
|
130
|
+
async function fetchReleaseByTag(expected) {
|
|
72
131
|
const response = await fetch(REPO_API_URL, {
|
|
73
132
|
headers: {
|
|
74
133
|
"Accept": "application/vnd.github+json",
|
|
@@ -77,7 +136,7 @@ async function fetchLatestRelease() {
|
|
|
77
136
|
});
|
|
78
137
|
|
|
79
138
|
if (!response.ok) {
|
|
80
|
-
throw new Error(`GitHub Releases API returned ${response.status} ${response.statusText}`);
|
|
139
|
+
throw new Error(`GitHub Releases API returned ${response.status} ${response.statusText} for ${expected.tagName}`);
|
|
81
140
|
}
|
|
82
141
|
|
|
83
142
|
const release = await response.json();
|
|
@@ -86,13 +145,16 @@ async function fetchLatestRelease() {
|
|
|
86
145
|
: undefined;
|
|
87
146
|
|
|
88
147
|
if (!release.tag_name || !asset?.browser_download_url) {
|
|
89
|
-
throw new Error(`
|
|
148
|
+
throw new Error(`Release ${expected.tagName} does not contain ${ASSET_NAME}`);
|
|
90
149
|
}
|
|
91
150
|
|
|
92
151
|
const tagName = String(release.tag_name);
|
|
93
152
|
if (!/^v\d+\.\d+\.\d+$/.test(tagName)) {
|
|
94
153
|
throw new Error(`Unexpected tag format: ${tagName}`);
|
|
95
154
|
}
|
|
155
|
+
if (tagName !== expected.tagName) {
|
|
156
|
+
throw new Error(`Unexpected tag: expected ${expected.tagName}, got ${tagName}`);
|
|
157
|
+
}
|
|
96
158
|
|
|
97
159
|
return {
|
|
98
160
|
tagName,
|
|
@@ -100,6 +162,25 @@ async function fetchLatestRelease() {
|
|
|
100
162
|
};
|
|
101
163
|
}
|
|
102
164
|
|
|
165
|
+
async function sha256File(filePath) {
|
|
166
|
+
const hash = createHash("sha256");
|
|
167
|
+
await new Promise((resolve, reject) => {
|
|
168
|
+
const stream = createReadStream(filePath);
|
|
169
|
+
stream.on("error", reject);
|
|
170
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
171
|
+
stream.on("end", resolve);
|
|
172
|
+
});
|
|
173
|
+
return hash.digest("hex").toLowerCase();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function verifySha256(filePath, expectedSha256) {
|
|
177
|
+
const actual = await sha256File(filePath);
|
|
178
|
+
const expected = String(expectedSha256).toLowerCase();
|
|
179
|
+
if (actual !== expected) {
|
|
180
|
+
throw new Error(`SHA256 mismatch for ${ASSET_NAME}: expected ${expected}, got ${actual}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
103
184
|
async function downloadFile(url, destination) {
|
|
104
185
|
const response = await fetch(url, {
|
|
105
186
|
headers: {
|
|
@@ -144,19 +225,19 @@ async function expandZip(zipPath, destination) {
|
|
|
144
225
|
}
|
|
145
226
|
|
|
146
227
|
async function findExtractedRoot(extractDir) {
|
|
147
|
-
if (
|
|
228
|
+
if (existsSync(path.join(extractDir, "dist", "index.js"))) return extractDir;
|
|
148
229
|
|
|
149
230
|
const entries = await readdir(extractDir, { withFileTypes: true });
|
|
150
231
|
for (const entry of entries) {
|
|
151
232
|
if (!entry.isDirectory()) continue;
|
|
152
233
|
const candidate = path.join(extractDir, entry.name);
|
|
153
|
-
if (
|
|
234
|
+
if (existsSync(path.join(candidate, "dist", "index.js"))) return candidate;
|
|
154
235
|
}
|
|
155
236
|
|
|
156
237
|
throw new Error("Release zip did not contain dist/index.js");
|
|
157
238
|
}
|
|
158
239
|
|
|
159
|
-
async function installRelease(release) {
|
|
240
|
+
async function installRelease(release, expected) {
|
|
160
241
|
await mkdir(RELEASES_DIR, { recursive: true });
|
|
161
242
|
|
|
162
243
|
const targetDir = releaseDirForTag(release.tagName);
|
|
@@ -167,13 +248,15 @@ async function installRelease(release) {
|
|
|
167
248
|
try {
|
|
168
249
|
log(`Downloading ${ASSET_NAME} from ${release.tagName}`);
|
|
169
250
|
await downloadFile(release.assetUrl, zipPath);
|
|
251
|
+
await verifySha256(zipPath, expected.sha256);
|
|
170
252
|
await mkdir(extractDir, { recursive: true });
|
|
171
253
|
await expandZip(zipPath, extractDir);
|
|
172
254
|
|
|
173
255
|
const extractedRoot = await findExtractedRoot(extractDir);
|
|
174
256
|
await rm(targetDir, { recursive: true, force: true });
|
|
175
257
|
await rename(extractedRoot, targetDir);
|
|
176
|
-
await
|
|
258
|
+
await writeReleaseMetadata(targetDir, expected);
|
|
259
|
+
await writeCurrentRelease(expected);
|
|
177
260
|
log(`Installed ${release.tagName} to ${targetDir}`);
|
|
178
261
|
return targetDir;
|
|
179
262
|
} finally {
|
|
@@ -182,26 +265,21 @@ async function installRelease(release) {
|
|
|
182
265
|
}
|
|
183
266
|
|
|
184
267
|
async function ensureRelease() {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
} catch (error) {
|
|
191
|
-
if (current) {
|
|
192
|
-
log(`Could not check GitHub Releases; using cached ${current.tagName}`);
|
|
193
|
-
return current.releaseDir;
|
|
194
|
-
}
|
|
195
|
-
throw error;
|
|
268
|
+
const expected = expectedReleaseSpec();
|
|
269
|
+
const targetDir = releaseDirForTag(expected.tagName);
|
|
270
|
+
if (await isInstalled(targetDir, expected)) {
|
|
271
|
+
await writeCurrentRelease(expected);
|
|
272
|
+
return targetDir;
|
|
196
273
|
}
|
|
197
274
|
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
return targetDir;
|
|
275
|
+
const current = await readCurrentRelease(expected);
|
|
276
|
+
if (current) {
|
|
277
|
+
return current.releaseDir;
|
|
202
278
|
}
|
|
203
279
|
|
|
204
|
-
|
|
280
|
+
const release = await fetchReleaseByTag(expected);
|
|
281
|
+
|
|
282
|
+
return installRelease(release, expected);
|
|
205
283
|
}
|
|
206
284
|
|
|
207
285
|
function launchServer(releaseDir) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harusame64/desktop-touch-mcp",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.5",
|
|
4
4
|
"mcpName": "io.github.Harusame64/desktop-touch-mcp",
|
|
5
5
|
"description": "LLM-native Windows computer-use MCP server with 56 tools for screenshots, UIA, mouse/keyboard, Chrome CDP, terminal, SmartScroll, and perception guards",
|
|
6
6
|
"engines": {
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc",
|
|
43
43
|
"sync-version": "node scripts/sync-version.mjs",
|
|
44
|
-
"
|
|
44
|
+
"check:launcher-manifest": "node scripts/check-launcher-manifest.mjs",
|
|
45
|
+
"version": "npm run sync-version && git add src/version.ts bin/launcher.js",
|
|
45
46
|
"prepare": "tsc",
|
|
46
47
|
"start": "node dist/index.js",
|
|
47
48
|
"dev": "tsc --watch",
|