@zhihand/mcp 0.23.0 โ†’ 0.24.1

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.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ZhiHand MCP Server โ€” let AI agents see and control your phone.
4
4
 
5
- Version: `0.23.0`
5
+ Version: `0.24.1`
6
6
 
7
7
  ## What is this?
8
8
 
@@ -96,6 +96,7 @@ zhihand start -d Start daemon in background (logs to ~/.zhihand/daemon
96
96
  zhihand stop Stop the running daemon
97
97
  zhihand status Show daemon status, pairing info, device, backend, and model
98
98
 
99
+ zhihand test Test device connectivity (screenshot, click, swipe, home, back)
99
100
  zhihand pair Pair with a phone (QR code in terminal)
100
101
  zhihand detect List detected CLI tools and their login status
101
102
  zhihand serve Start MCP Server (stdio mode, backward compatible)
package/bin/zhihand CHANGED
@@ -51,6 +51,7 @@ Usage:
51
51
  zhihand pair Pair with a phone device
52
52
  zhihand detect Detect available CLI tools
53
53
 
54
+ zhihand test Test device connectivity (sends click + type commands)
54
55
  zhihand serve Start MCP Server (stdio mode, backward compat)
55
56
 
56
57
  Options:
@@ -280,6 +281,87 @@ switch (command) {
280
281
  break;
281
282
  }
282
283
 
284
+ case "test": {
285
+ const { resolveConfig: resolveTestConfig } = await import("../dist/core/config.js");
286
+ const { createControlCommand, enqueueCommand } = await import("../dist/core/command.js");
287
+ const { waitForCommandAck } = await import("../dist/core/sse.js");
288
+ const { fetchScreenshotBinary } = await import("../dist/core/screenshot.js");
289
+
290
+ let testConfig;
291
+ try {
292
+ testConfig = resolveTestConfig(values.device ?? process.env.ZHIHAND_DEVICE);
293
+ } catch (err) {
294
+ console.error(`Error: ${err.message}`);
295
+ console.error("Run 'zhihand setup' to pair a device first.");
296
+ process.exit(1);
297
+ }
298
+
299
+ console.log("๐Ÿงช ZhiHand Device Test");
300
+ console.log(` Device: ${testConfig.credentialId}`);
301
+ console.log(` Endpoint: ${testConfig.controlPlaneEndpoint}\n`);
302
+
303
+ const steps = [
304
+ { label: "1. Screenshot", type: "screenshot" },
305
+ { label: "2. Click center", type: "hid", params: { action: "click", xRatio: 0.5, yRatio: 0.5 } },
306
+ { label: "3. Swipe up", type: "hid", params: { action: "swipe", startXRatio: 0.5, startYRatio: 0.7, endXRatio: 0.5, endYRatio: 0.3, durationMs: 300 } },
307
+ { label: "4. Swipe down", type: "hid", params: { action: "swipe", startXRatio: 0.5, startYRatio: 0.3, endXRatio: 0.5, endYRatio: 0.7, durationMs: 300 } },
308
+ { label: "5. Press Home", type: "hid", params: { action: "home" } },
309
+ { label: "6. Press Back", type: "hid", params: { action: "back" } },
310
+ { label: "7. Screenshot", type: "screenshot" },
311
+ ];
312
+
313
+ let passed = 0;
314
+ let failed = 0;
315
+
316
+ for (const step of steps) {
317
+ process.stdout.write(` ${step.label}... `);
318
+ const t0 = Date.now();
319
+ try {
320
+ if (step.type === "screenshot") {
321
+ // Screenshot: send receive_screenshot command, then fetch binary
322
+ const cmd = createControlCommand({ action: "screenshot" });
323
+ const queued = await enqueueCommand(testConfig, cmd);
324
+ const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
325
+ if (ack.acked) {
326
+ const buf = await fetchScreenshotBinary(testConfig);
327
+ const ms = Date.now() - t0;
328
+ console.log(`โœ… ${(buf.length / 1024).toFixed(0)}KB (${ms}ms)`);
329
+ passed++;
330
+ } else {
331
+ console.log(`โฑ๏ธ Timeout (${Date.now() - t0}ms)`);
332
+ failed++;
333
+ }
334
+ } else {
335
+ const cmd = createControlCommand(step.params);
336
+ const queued = await enqueueCommand(testConfig, cmd);
337
+ const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
338
+ const ms = Date.now() - t0;
339
+ if (ack.acked) {
340
+ console.log(`โœ… (${ms}ms)`);
341
+ passed++;
342
+ } else {
343
+ console.log(`โฑ๏ธ Timeout (${ms}ms)`);
344
+ failed++;
345
+ }
346
+ }
347
+ } catch (err) {
348
+ const ms = Date.now() - t0;
349
+ console.log(`โŒ ${err.message} (${ms}ms)`);
350
+ failed++;
351
+ }
352
+ // Pause between commands: let phone process + user can observe
353
+ await new Promise((r) => setTimeout(r, 2000));
354
+ }
355
+
356
+ console.log(`\n Result: ${passed}/${steps.length} passed`);
357
+ if (failed === 0) {
358
+ console.log(" โœ… Device is responding to all commands!");
359
+ } else {
360
+ console.log(" โš ๏ธ Some commands failed. Check phone connectivity.");
361
+ }
362
+ process.exit(failed > 0 ? 1 : 0);
363
+ }
364
+
283
365
  default:
284
366
  console.error(`Unknown command: ${command}. Run 'zhihand --help' for usage.`);
285
367
  process.exit(1);
@@ -7,6 +7,8 @@ import { DEFAULT_MODELS } from "../core/config.js";
7
7
  import { resolveGemini, resolveClaude, resolveCodex } from "../core/resolve-path.js";
8
8
  const CLI_TIMEOUT = 300_000; // 300s (5min) per prompt โ€” MCP tool chains need multiple turns
9
9
  const SIGKILL_DELAY = 2_000; // 2s after SIGTERM
10
+ const MCP_PORT = parseInt(process.env.ZHIHAND_PORT ?? "", 10) || 18686;
11
+ const MCP_URL = `http://127.0.0.1:${MCP_PORT}/mcp`;
10
12
  const MAX_OUTPUT_BYTES = 100 * 1024; // 100KB (for one-shot backends)
11
13
  const MAX_HISTORY_TURNS = 20; // keep last N exchanges in conversation history
12
14
  // Gemini session file polling
@@ -540,11 +542,14 @@ async function dispatchClaudeWithHistory(prompt, startTime, log, model) {
540
542
  log(`[claude] One-shot dispatch (history: ${conversationHistory.length} turns)`);
541
543
  // Pass prompt via stdin (-p -) to avoid ARG_MAX limit with long conversation history
542
544
  // --permission-mode bypassPermissions: auto-approve all tool calls (like gemini's --approval-mode yolo)
545
+ // --mcp-config: explicitly pass MCP server URL so Claude finds it regardless of cwd
546
+ const mcpConfig = JSON.stringify({ mcpServers: { zhihand: { type: "http", url: MCP_URL } } });
543
547
  const child = spawn(claudePath, [
544
548
  "-p", "-",
545
549
  "--model", model,
546
550
  "--output-format", "json",
547
551
  "--permission-mode", "bypassPermissions",
552
+ "--mcp-config", mcpConfig,
548
553
  ], {
549
554
  env: process.env,
550
555
  stdio: ["pipe", "pipe", "pipe"],
@@ -553,11 +558,29 @@ async function dispatchClaudeWithHistory(prompt, startTime, log, model) {
553
558
  // Write prompt to stdin, then close to signal EOF
554
559
  child.stdin?.write(fullPrompt);
555
560
  child.stdin?.end();
556
- const result = await collectChildOutput(child, startTime);
561
+ const raw = await collectChildOutput(child, startTime);
562
+ // Claude --output-format json wraps the result in a JSON envelope โ€” extract the actual text
563
+ const result = extractClaudeResult(raw);
557
564
  recordTurn("user", prompt);
558
565
  recordTurn("assistant", result.text);
559
566
  return result;
560
567
  }
568
+ /** Parse Claude JSON output and extract the result text. */
569
+ function extractClaudeResult(raw) {
570
+ try {
571
+ const parsed = JSON.parse(raw.text);
572
+ if (!parsed || typeof parsed !== "object")
573
+ return raw;
574
+ const resultText = typeof parsed.result === "string" ? parsed.result : raw.text;
575
+ const isError = parsed.is_error === true || parsed.subtype === "error";
576
+ // Preserve process exit failure: only succeed if both JSON and process agree
577
+ return { text: resultText, success: raw.success && !isError, durationMs: raw.durationMs };
578
+ }
579
+ catch {
580
+ // Not JSON โ€” return as-is
581
+ return raw;
582
+ }
583
+ }
561
584
  // โ”€โ”€ Codex JSONL Output Collector โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
562
585
  function collectCodexOutput(child, startTime) {
563
586
  return new Promise((resolve) => {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare const PACKAGE_VERSION = "0.23.0";
2
+ export declare const PACKAGE_VERSION = "0.24.1";
3
3
  export declare function createServer(deviceName?: string): McpServer;
4
4
  export declare function startStdioServer(deviceName?: string): Promise<void>;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { controlSchema, screenshotSchema, pairSchema } from "./tools/schemas.js"
5
5
  import { executeControl } from "./tools/control.js";
6
6
  import { handleScreenshot } from "./tools/screenshot.js";
7
7
  import { handlePair } from "./tools/pair.js";
8
- export const PACKAGE_VERSION = "0.23.0";
8
+ export const PACKAGE_VERSION = "0.24.1";
9
9
  export function createServer(deviceName) {
10
10
  const server = new McpServer({
11
11
  name: "zhihand",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhihand/mcp",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
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",