@zhihand/mcp 0.29.0 → 0.32.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/bin/zhihand +448 -212
- package/dist/core/command.d.ts +5 -5
- package/dist/core/command.js +6 -8
- package/dist/core/config.d.ts +48 -21
- package/dist/core/config.js +178 -42
- package/dist/core/device.d.ts +28 -19
- package/dist/core/device.js +168 -145
- package/dist/core/logger.d.ts +17 -0
- package/dist/core/logger.js +32 -0
- package/dist/core/pair.d.ts +39 -31
- package/dist/core/pair.js +205 -77
- package/dist/core/registry.d.ts +60 -0
- package/dist/core/registry.js +415 -0
- package/dist/core/screenshot.d.ts +3 -3
- package/dist/core/screenshot.js +3 -2
- package/dist/core/sse.d.ts +40 -18
- package/dist/core/sse.js +122 -62
- package/dist/core/ws.d.ts +92 -0
- package/dist/core/ws.js +327 -0
- package/dist/daemon/dispatcher.d.ts +3 -1
- package/dist/daemon/dispatcher.js +4 -3
- package/dist/daemon/heartbeat.d.ts +4 -4
- package/dist/daemon/heartbeat.js +1 -1
- package/dist/daemon/index.js +10 -8
- package/dist/daemon/prompt-listener.d.ts +8 -7
- package/dist/daemon/prompt-listener.js +59 -99
- package/dist/index.d.ts +3 -3
- package/dist/index.js +104 -40
- package/dist/openclaw.adapter.js +10 -2
- package/dist/tools/control.d.ts +10 -3
- package/dist/tools/control.js +18 -24
- package/dist/tools/pair.d.ts +1 -1
- package/dist/tools/pair.js +22 -28
- package/dist/tools/resolve.d.ts +7 -0
- package/dist/tools/resolve.js +22 -0
- package/dist/tools/schemas.d.ts +9 -1
- package/dist/tools/schemas.js +10 -8
- package/dist/tools/screenshot.d.ts +3 -2
- package/dist/tools/screenshot.js +2 -2
- package/dist/tools/system.d.ts +3 -5
- package/dist/tools/system.js +19 -6
- package/package.json +3 -1
package/dist/tools/control.js
CHANGED
|
@@ -1,40 +1,36 @@
|
|
|
1
1
|
import { createControlCommand, enqueueCommand, formatAckSummary } from "../core/command.js";
|
|
2
2
|
import { fetchScreenshot } from "../core/screenshot.js";
|
|
3
|
-
import { waitForCommandAck } from "../core/
|
|
4
|
-
import { getCapabilities, isDeviceProfileLoaded } from "../core/device.js";
|
|
3
|
+
import { waitForCommandAck } from "../core/ws.js";
|
|
5
4
|
function sleep(ms) {
|
|
6
5
|
return new Promise((r) => setTimeout(r, ms));
|
|
7
6
|
}
|
|
8
7
|
/**
|
|
9
8
|
* Build a short human-readable warning for the LLM if the underlying
|
|
10
|
-
* capability isn't ready, or if the last screenshot is stale.
|
|
11
|
-
* empty string when everything is nominal.
|
|
9
|
+
* capability isn't ready, or if the last screenshot is stale.
|
|
12
10
|
*/
|
|
13
|
-
function buildReadinessWarning(requiredCapability, screenshot) {
|
|
14
|
-
if (!
|
|
11
|
+
export function buildReadinessWarning(requiredCapability, capabilities, screenshot) {
|
|
12
|
+
if (!capabilities)
|
|
15
13
|
return "";
|
|
16
|
-
const caps = getCapabilities();
|
|
17
14
|
const warnings = [];
|
|
18
|
-
if (requiredCapability === "hid" && !
|
|
19
|
-
warnings.push(`⚠️ HID not ready: ${
|
|
15
|
+
if (requiredCapability === "hid" && !capabilities.hid.ready) {
|
|
16
|
+
warnings.push(`⚠️ HID not ready: ${capabilities.hid.reason}`);
|
|
20
17
|
}
|
|
21
|
-
if (requiredCapability === "screen" && !
|
|
22
|
-
warnings.push(`⚠️ Screen sharing not active: ${
|
|
18
|
+
if (requiredCapability === "screen" && !capabilities.screen_sharing.ready) {
|
|
19
|
+
warnings.push(`⚠️ Screen sharing not active: ${capabilities.screen_sharing.reason}`);
|
|
23
20
|
}
|
|
24
21
|
if (screenshot && screenshot.stale) {
|
|
25
22
|
warnings.push(`⚠️ Stale screenshot: age=${(screenshot.ageMs / 1000).toFixed(1)}s (phone may not be actively sharing the screen).`);
|
|
26
23
|
}
|
|
27
|
-
if (
|
|
28
|
-
warnings.push(`⚠️ Stale device profile: ${(
|
|
24
|
+
if (capabilities.profile.stale) {
|
|
25
|
+
warnings.push(`⚠️ Stale device profile: ${(capabilities.profile.age_ms / 1000).toFixed(1)}s old — readiness flags may be out of date.`);
|
|
29
26
|
}
|
|
30
27
|
return warnings.join("\n");
|
|
31
28
|
}
|
|
32
|
-
export async function executeControl(config, params) {
|
|
33
|
-
// wait: Plugin-local implementation, no server round-trip
|
|
29
|
+
export async function executeControl(config, params, platform, capabilities) {
|
|
34
30
|
if (params.action === "wait") {
|
|
35
31
|
await sleep(params.durationMs ?? 1000);
|
|
36
32
|
const shot = await fetchScreenshot(config);
|
|
37
|
-
const warning = buildReadinessWarning("screen", shot);
|
|
33
|
+
const warning = buildReadinessWarning("screen", capabilities, shot);
|
|
38
34
|
const content = [];
|
|
39
35
|
if (warning)
|
|
40
36
|
content.push({ type: "text", text: warning });
|
|
@@ -42,12 +38,10 @@ export async function executeControl(config, params) {
|
|
|
42
38
|
content.push({ type: "image", data: shot.buffer.toString("base64"), mimeType: "image/jpeg" });
|
|
43
39
|
return { content };
|
|
44
40
|
}
|
|
45
|
-
// screenshot: send receive_screenshot, App captures immediately (no 2s delay)
|
|
46
41
|
if (params.action === "screenshot") {
|
|
47
|
-
return await executeScreenshot(config);
|
|
42
|
+
return await executeScreenshot(config, capabilities);
|
|
48
43
|
}
|
|
49
|
-
|
|
50
|
-
const command = createControlCommand(params);
|
|
44
|
+
const command = createControlCommand(params, platform);
|
|
51
45
|
const queued = await enqueueCommand(config, command);
|
|
52
46
|
const ack = await waitForCommandAck(config, { commandId: queued.id, timeoutMs: 15_000 });
|
|
53
47
|
const content = [];
|
|
@@ -57,10 +51,10 @@ export async function executeControl(config, params) {
|
|
|
57
51
|
shot = await fetchScreenshot(config);
|
|
58
52
|
}
|
|
59
53
|
catch {
|
|
60
|
-
//
|
|
54
|
+
// best-effort
|
|
61
55
|
}
|
|
62
56
|
}
|
|
63
|
-
const warning = buildReadinessWarning("hid", shot);
|
|
57
|
+
const warning = buildReadinessWarning("hid", capabilities, shot);
|
|
64
58
|
if (warning)
|
|
65
59
|
content.push({ type: "text", text: warning });
|
|
66
60
|
content.push({ type: "text", text: formatAckSummary(params.action, ack) });
|
|
@@ -69,12 +63,12 @@ export async function executeControl(config, params) {
|
|
|
69
63
|
}
|
|
70
64
|
return { content };
|
|
71
65
|
}
|
|
72
|
-
export async function executeScreenshot(config) {
|
|
66
|
+
export async function executeScreenshot(config, capabilities) {
|
|
73
67
|
const command = createControlCommand({ action: "screenshot" });
|
|
74
68
|
const queued = await enqueueCommand(config, command);
|
|
75
69
|
const ack = await waitForCommandAck(config, { commandId: queued.id, timeoutMs: 5_000 });
|
|
76
70
|
const shot = await fetchScreenshot(config);
|
|
77
|
-
const warning = buildReadinessWarning("screen", shot);
|
|
71
|
+
const warning = buildReadinessWarning("screen", capabilities, shot);
|
|
78
72
|
const content = [];
|
|
79
73
|
if (warning)
|
|
80
74
|
content.push({ type: "text", text: warning });
|
package/dist/tools/pair.d.ts
CHANGED
package/dist/tools/pair.js
CHANGED
|
@@ -1,32 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createPairingSession, registerPlugin, renderPairingQRCode,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
],
|
|
18
|
-
};
|
|
19
|
-
}
|
|
1
|
+
import { listUsers } from "../core/config.js";
|
|
2
|
+
import { createPairingSession, registerPlugin, renderPairingQRCode, createUser, } from "../core/pair.js";
|
|
3
|
+
import { resolveDefaultEndpoint } from "../core/config.js";
|
|
4
|
+
export async function handlePair(params) {
|
|
5
|
+
const endpoint = resolveDefaultEndpoint();
|
|
6
|
+
const users = listUsers();
|
|
7
|
+
if (!params.forceNew && users.length > 0) {
|
|
8
|
+
const lines = [
|
|
9
|
+
"Already paired with:",
|
|
10
|
+
"",
|
|
11
|
+
...users.map((u) => ` User: ${u.label} (${u.user_id})\n` +
|
|
12
|
+
u.devices.map((d) => ` - ${d.credential_id} (${d.label}, ${d.platform})`).join("\n")),
|
|
13
|
+
"",
|
|
14
|
+
"Pass forceNew=true to pair another device.",
|
|
15
|
+
];
|
|
16
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
20
17
|
}
|
|
21
|
-
//
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const session = await createPairingSession(resolvedEndpoint, {
|
|
28
|
-
edgeId: plugin.edge_id,
|
|
29
|
-
});
|
|
18
|
+
// Create a new user for MCP-tool pairing
|
|
19
|
+
const label = `MCP-${Date.now().toString(36)}`;
|
|
20
|
+
const userResp = await createUser(endpoint, label);
|
|
21
|
+
const stableIdentity = `mcp-${Date.now().toString(36)}`;
|
|
22
|
+
const plugin = await registerPlugin(endpoint, { stableIdentity });
|
|
23
|
+
const session = await createPairingSession(endpoint, userResp.user_id, userResp.controller_token, plugin.edge_id, 300);
|
|
30
24
|
const qr = await renderPairingQRCode(session.pair_url);
|
|
31
25
|
return {
|
|
32
26
|
content: [
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { registry } from "../core/registry.js";
|
|
2
|
+
export function resolveTargetDevice(deviceId) {
|
|
3
|
+
const online = registry.listOnline();
|
|
4
|
+
if (deviceId) {
|
|
5
|
+
const s = registry.get(deviceId);
|
|
6
|
+
if (!s) {
|
|
7
|
+
return { error: `Device '${deviceId}' not found. Call zhihand_list_devices.` };
|
|
8
|
+
}
|
|
9
|
+
if (!s.online) {
|
|
10
|
+
return { error: `Device '${deviceId}' is offline. Call zhihand_list_devices for online devices.` };
|
|
11
|
+
}
|
|
12
|
+
return { state: s };
|
|
13
|
+
}
|
|
14
|
+
if (online.length === 0) {
|
|
15
|
+
return { error: "No devices online. Ask user to open the ZhiHand app." };
|
|
16
|
+
}
|
|
17
|
+
if (online.length === 1) {
|
|
18
|
+
return { state: online[0] };
|
|
19
|
+
}
|
|
20
|
+
const ids = online.map((d) => `${d.credentialId} (${d.label})`).join(", ");
|
|
21
|
+
return { error: `Multiple devices online — pass device_id. Online: ${ids}` };
|
|
22
|
+
}
|
package/dist/tools/schemas.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const controlSchema: {
|
|
3
|
+
device_id: z.ZodOptional<z.ZodString>;
|
|
3
4
|
action: z.ZodEnum<["click", "doubleclick", "longclick", "rightclick", "middleclick", "type", "swipe", "scroll", "keycombo", "back", "home", "enter", "clipboard", "open_app", "wait", "screenshot"]>;
|
|
4
5
|
xRatio: z.ZodOptional<z.ZodNumber>;
|
|
5
6
|
yRatio: z.ZodOptional<z.ZodNumber>;
|
|
@@ -17,10 +18,17 @@ export declare const controlSchema: {
|
|
|
17
18
|
urlScheme: z.ZodOptional<z.ZodString>;
|
|
18
19
|
};
|
|
19
20
|
export declare const systemSchema: {
|
|
21
|
+
device_id: z.ZodOptional<z.ZodString>;
|
|
20
22
|
action: z.ZodEnum<["notification", "recent", "search", "switch_input", "siri", "control_center", "open_browser", "shortcut_help", "volume_up", "volume_down", "mute", "play_pause", "stop", "next_track", "prev_track", "fast_forward", "rewind", "brightness_up", "brightness_down", "power"]>;
|
|
21
23
|
text: z.ZodOptional<z.ZodString>;
|
|
22
24
|
};
|
|
23
|
-
export declare const screenshotSchema: {
|
|
25
|
+
export declare const screenshotSchema: {
|
|
26
|
+
device_id: z.ZodOptional<z.ZodString>;
|
|
27
|
+
};
|
|
24
28
|
export declare const pairSchema: {
|
|
25
29
|
forceNew: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
26
30
|
};
|
|
31
|
+
export declare const listDevicesSchema: {};
|
|
32
|
+
export declare const statusSchema: {
|
|
33
|
+
device_id: z.ZodOptional<z.ZodString>;
|
|
34
|
+
};
|
package/dist/tools/schemas.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
const deviceIdSchema = z.string().optional().describe("Credential ID of the target device. Optional if only one device is online. Call zhihand_list_devices to see online devices.");
|
|
2
3
|
export const controlSchema = {
|
|
4
|
+
device_id: deviceIdSchema,
|
|
3
5
|
action: z.enum([
|
|
4
6
|
"click", "doubleclick", "longclick", "rightclick", "middleclick",
|
|
5
7
|
"type", "swipe", "scroll", "keycombo",
|
|
@@ -13,7 +15,6 @@ export const controlSchema = {
|
|
|
13
15
|
direction: z.enum(["up", "down", "left", "right"]).optional().describe("Scroll direction"),
|
|
14
16
|
amount: z.number().int().positive().default(3).optional().describe("Scroll steps (default 3)"),
|
|
15
17
|
keys: z.string().optional().describe("Key combo string, e.g. 'ctrl+c', 'alt+tab'"),
|
|
16
|
-
// clipboardAction removed — app only supports set (text via "text" param)
|
|
17
18
|
durationMs: z.number().int().positive().max(10000).optional().describe("Duration in ms: wait (default 1000), longclick (default 800), swipe (default 300). Max 10000"),
|
|
18
19
|
startXRatio: z.number().min(0).max(1).optional().describe("Swipe start X [0,1]"),
|
|
19
20
|
startYRatio: z.number().min(0).max(1).optional().describe("Swipe start Y [0,1]"),
|
|
@@ -23,25 +24,26 @@ export const controlSchema = {
|
|
|
23
24
|
bundleId: z.string().optional().describe("iOS bundle ID, e.g. 'com.tencent.xin'"),
|
|
24
25
|
urlScheme: z.string().optional().describe("URL scheme, e.g. 'weixin://'"),
|
|
25
26
|
};
|
|
26
|
-
// zhihand_system — system navigation + media controls (separate from UI control)
|
|
27
27
|
export const systemSchema = {
|
|
28
|
+
device_id: deviceIdSchema,
|
|
28
29
|
action: z.enum([
|
|
29
|
-
// System navigation — cross-platform
|
|
30
30
|
"notification", "recent", "search", "switch_input",
|
|
31
|
-
// System navigation — iOS only
|
|
32
31
|
"siri", "control_center",
|
|
33
|
-
// System navigation — Android only
|
|
34
32
|
"open_browser", "shortcut_help",
|
|
35
|
-
// Media controls — cross-platform
|
|
36
33
|
"volume_up", "volume_down", "mute",
|
|
37
34
|
"play_pause", "stop", "next_track", "prev_track",
|
|
38
35
|
"fast_forward", "rewind",
|
|
39
|
-
// Hardware — cross-platform
|
|
40
36
|
"brightness_up", "brightness_down", "power",
|
|
41
37
|
]).describe("System or media action to perform"),
|
|
42
38
|
text: z.string().optional().describe("Optional text, e.g. search query for 'search' action"),
|
|
43
39
|
};
|
|
44
|
-
export const screenshotSchema = {
|
|
40
|
+
export const screenshotSchema = {
|
|
41
|
+
device_id: deviceIdSchema,
|
|
42
|
+
};
|
|
45
43
|
export const pairSchema = {
|
|
46
44
|
forceNew: z.boolean().default(false).optional().describe("Force new pairing even if already paired"),
|
|
47
45
|
};
|
|
46
|
+
export const listDevicesSchema = {};
|
|
47
|
+
export const statusSchema = {
|
|
48
|
+
device_id: deviceIdSchema,
|
|
49
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { ZhiHandRuntimeConfig } from "../core/config.ts";
|
|
2
|
+
import type { Capabilities } from "../core/device.ts";
|
|
3
|
+
export declare function handleScreenshot(config: ZhiHandRuntimeConfig, capabilities: Capabilities | null): Promise<{
|
|
3
4
|
content: ({
|
|
4
5
|
type: "text";
|
|
5
6
|
text: string;
|
package/dist/tools/screenshot.js
CHANGED
package/dist/tools/system.d.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* zhihand_system tool handler — system navigation + media controls.
|
|
3
|
-
*
|
|
4
|
-
* Separated from zhihand_control to keep UI-control schema focused and
|
|
5
|
-
* reduce LLM parameter hallucination (Gemini design review recommendation).
|
|
6
3
|
*/
|
|
7
|
-
import type {
|
|
4
|
+
import type { ZhiHandRuntimeConfig } from "../core/config.ts";
|
|
8
5
|
import type { SystemParams } from "../core/command.ts";
|
|
9
6
|
type TextContent = {
|
|
10
7
|
type: "text";
|
|
@@ -12,6 +9,7 @@ type TextContent = {
|
|
|
12
9
|
};
|
|
13
10
|
type ToolResult = {
|
|
14
11
|
content: TextContent[];
|
|
12
|
+
isError?: boolean;
|
|
15
13
|
};
|
|
16
|
-
export declare function executeSystem(config:
|
|
14
|
+
export declare function executeSystem(config: ZhiHandRuntimeConfig, params: SystemParams, platform: string): Promise<ToolResult>;
|
|
17
15
|
export {};
|
package/dist/tools/system.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { createSystemCommand, enqueueCommand, formatAckSummary } from "../core/command.js";
|
|
2
|
-
import { waitForCommandAck } from "../core/
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { waitForCommandAck } from "../core/ws.js";
|
|
3
|
+
const IOS_ONLY = new Set(["siri", "control_center"]);
|
|
4
|
+
const ANDROID_ONLY = new Set(["open_browser", "shortcut_help"]);
|
|
5
|
+
export async function executeSystem(config, params, platform) {
|
|
6
|
+
// Platform guard
|
|
7
|
+
if (platform !== "ios" && IOS_ONLY.has(params.action)) {
|
|
8
|
+
return {
|
|
9
|
+
content: [{ type: "text", text: `Action ${params.action} is not supported on platform ${platform}` }],
|
|
10
|
+
isError: true,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (platform !== "android" && ANDROID_ONLY.has(params.action)) {
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: `Action ${params.action} is not supported on platform ${platform}` }],
|
|
16
|
+
isError: true,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const command = createSystemCommand(params, platform);
|
|
5
20
|
const queued = await enqueueCommand(config, command);
|
|
6
21
|
const ack = await waitForCommandAck(config, { commandId: queued.id, timeoutMs: 15_000 });
|
|
7
22
|
const summary = formatAckSummary(params.action, ack);
|
|
8
|
-
return {
|
|
9
|
-
content: [{ type: "text", text: summary }],
|
|
10
|
-
};
|
|
23
|
+
return { content: [{ type: "text", text: summary }] };
|
|
11
24
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhihand/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "ZhiHand MCP Server — phone control tools for Claude Code, Codex, Gemini CLI, and OpenClaw",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
40
40
|
"qrcode": "^1.5.4",
|
|
41
|
+
"ws": "^8.20.0",
|
|
41
42
|
"zod": "^3.24.0"
|
|
42
43
|
},
|
|
43
44
|
"engines": {
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/node": "^25.5.0",
|
|
48
49
|
"@types/qrcode": "^1.5.6",
|
|
50
|
+
"@types/ws": "^8.18.1",
|
|
49
51
|
"typescript": "^6.0.2"
|
|
50
52
|
}
|
|
51
53
|
}
|