@zhihand/mcp 0.12.3 → 0.15.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/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.12.1`
5
+ Version: `0.15.0`
6
6
 
7
7
  ## What is this?
8
8
 
@@ -39,7 +39,7 @@ npx @zhihand/mcp serve
39
39
 
40
40
  ## Quick Start
41
41
 
42
- ### 1. Pair your phone
42
+ ### 1. Setup and pair
43
43
 
44
44
  ```bash
45
45
  zhihand setup
@@ -52,64 +52,11 @@ This runs the full interactive setup:
52
52
  3. Waits for you to scan the QR code with the ZhiHand mobile app
53
53
  4. Saves credentials to `~/.zhihand/credentials.json`
54
54
  5. Detects installed CLI tools (Claude Code, Codex, Gemini CLI, OpenClaw)
55
- 6. Prints the MCP configuration snippet for your tools
55
+ 6. Auto-selects the best available tool and configures MCP automatically
56
56
 
57
- ### 2. Configure your AI tool
57
+ No manual MCP configuration needed — `zhihand setup` handles everything.
58
58
 
59
- Add the ZhiHand MCP server to your tool's configuration:
60
-
61
- **Claude Code** — Add to `.mcp.json` in your project root, or run:
62
-
63
- ```bash
64
- claude mcp add zhihand -- zhihand serve
65
- ```
66
-
67
- Or manually create/edit `.mcp.json`:
68
-
69
- ```json
70
- {
71
- "mcpServers": {
72
- "zhihand": {
73
- "command": "zhihand",
74
- "args": ["serve"]
75
- }
76
- }
77
- }
78
- ```
79
-
80
- **Codex CLI** — Add to your MCP config:
81
-
82
- ```json
83
- {
84
- "mcpServers": {
85
- "zhihand": {
86
- "command": "zhihand",
87
- "args": ["serve"]
88
- }
89
- }
90
- }
91
- ```
92
-
93
- **Gemini CLI** — Add to `~/.gemini/settings.json`:
94
-
95
- ```json
96
- {
97
- "mcpServers": {
98
- "zhihand": {
99
- "command": "zhihand",
100
- "args": ["serve"]
101
- }
102
- }
103
- }
104
- ```
105
-
106
- **OpenClaw** — Install the plugin directly:
107
-
108
- ```bash
109
- openclaw plugins install @zhihand/mcp
110
- ```
111
-
112
- ### 3. Start using it
59
+ ### 2. Start using it
113
60
 
114
61
  Once configured, your AI agent can use ZhiHand tools directly. For example, in Claude Code:
115
62
 
@@ -124,13 +71,32 @@ Once configured, your AI agent can use ZhiHand tools directly. For example, in C
124
71
 
125
72
  ```
126
73
  zhihand serve Start MCP Server (stdio mode, called by AI tools)
127
- zhihand setup Interactive setup: pair + detect tools + print config
74
+ zhihand setup Interactive setup: pair + auto-detect + auto-configure
128
75
  zhihand pair Pair with a phone (QR code in terminal)
129
- zhihand status Show current pairing status and device info
76
+ zhihand status Show pairing status, device info, and active backend
130
77
  zhihand detect List detected CLI tools and their login status
131
78
  zhihand --help Show help
79
+
80
+ zhihand claude Switch backend to Claude Code (auto-configures MCP)
81
+ zhihand codex Switch backend to Codex CLI (auto-configures MCP)
82
+ zhihand gemini Switch backend to Gemini CLI (auto-configures MCP)
83
+ ```
84
+
85
+ ### Switching Backends
86
+
87
+ Use `zhihand claude`, `zhihand codex`, or `zhihand gemini` to switch the active backend:
88
+
89
+ ```bash
90
+ zhihand gemini # Switch to Gemini CLI
91
+ zhihand claude # Switch to Claude Code
92
+ zhihand codex # Switch to Codex CLI
132
93
  ```
133
94
 
95
+ When you switch:
96
+ - MCP config is **automatically added** to the new backend
97
+ - MCP config is **automatically removed** from the previous backend
98
+ - If the tool is not installed, an error is shown
99
+
134
100
  ### Options
135
101
 
136
102
  | Option | Description |
@@ -217,6 +183,7 @@ Pairing credentials are stored at:
217
183
  ```
218
184
  ~/.zhihand/
219
185
  ├── credentials.json # Device credentials (credentialId, controllerToken, endpoint)
186
+ ├── backend.json # Active backend selection (claudecode/codex/gemini)
220
187
  └── state.json # Current pairing session state
221
188
  ```
222
189
 
@@ -261,6 +228,7 @@ packages/mcp/
261
228
  │ └── cli/
262
229
  │ ├── detect.ts # CLI tool detection (Claude Code, Codex, Gemini, OpenClaw)
263
230
  │ ├── spawn.ts # CLI process spawning (for mobile-initiated tasks)
231
+ │ ├── mcp-config.ts # MCP auto-configuration (add/remove per backend)
264
232
  │ └── openclaw.ts # OpenClaw auto-detect & plugin install
265
233
  ├── dist/ # Compiled JavaScript (shipped in npm package)
266
234
  ├── package.json
package/bin/zhihand CHANGED
@@ -5,15 +5,24 @@ import { parseArgs } from "node:util";
5
5
  import { startStdioServer } from "../dist/index.js";
6
6
  import { detectCLITools, formatDetectedTools } from "../dist/cli/detect.js";
7
7
  import { detectAndSetupOpenClaw } from "../dist/cli/openclaw.js";
8
- import { loadDefaultCredential } from "../dist/core/config.js";
8
+ import { loadDefaultCredential, loadBackendConfig, saveBackendConfig } from "../dist/core/config.js";
9
9
  import { executePairing } from "../dist/core/pair.js";
10
+ import { configureMCP, displayName } from "../dist/cli/mcp-config.js";
10
11
 
11
12
  const DEFAULT_ENDPOINT = "https://api.zhihand.com";
12
13
 
14
+ const CLI_TOOL_MAP = {
15
+ claude: "claudecode",
16
+ codex: "codex",
17
+ gemini: "gemini",
18
+ };
19
+
13
20
  const { positionals, values } = parseArgs({
14
21
  allowPositionals: true,
22
+ strict: false,
15
23
  options: {
16
24
  device: { type: "string" },
25
+ model: { type: "string" },
17
26
  http: { type: "boolean", default: false },
18
27
  help: { type: "boolean", short: "h", default: false },
19
28
  },
@@ -31,7 +40,11 @@ Usage:
31
40
  zhihand pair Pair with a phone device
32
41
  zhihand status Show pairing status and device info
33
42
  zhihand detect Detect available CLI tools
34
- zhihand setup Interactive setup: pair + configure
43
+ zhihand setup Interactive setup: pair + auto-configure
44
+
45
+ zhihand claude Switch backend to Claude Code
46
+ zhihand codex Switch backend to Codex CLI
47
+ zhihand gemini Switch backend to Gemini CLI
35
48
 
36
49
  Options:
37
50
  --device <name> Use a specific paired device
@@ -40,6 +53,43 @@ Options:
40
53
  process.exit(0);
41
54
  }
42
55
 
56
+ // Handle CLI tool subcommands: claude, codex, gemini → switch backend
57
+ if (Object.prototype.hasOwnProperty.call(CLI_TOOL_MAP, command)) {
58
+ const backendName = CLI_TOOL_MAP[command];
59
+ const tools = await detectCLITools();
60
+ const tool = tools.find((t) => t.name === backendName);
61
+
62
+ if (!tool) {
63
+ console.error(`Error: ${command} is not installed or not found in PATH.`);
64
+ console.error(`Install it first, then try again.`);
65
+ process.exit(1);
66
+ }
67
+
68
+ if (!tool.loggedIn) {
69
+ console.error(`Warning: ${command} is installed but not logged in.`);
70
+ }
71
+
72
+ const config = loadBackendConfig();
73
+ const previous = config.activeBackend;
74
+
75
+ if (previous === backendName) {
76
+ console.log(`Already using ${displayName(backendName)} as backend.`);
77
+ process.exit(0);
78
+ }
79
+
80
+ console.log(`Switching backend to ${displayName(backendName)}...`);
81
+ const { configured, removed } = configureMCP(backendName, previous);
82
+
83
+ if (configured) {
84
+ saveBackendConfig({ activeBackend: backendName });
85
+ console.log(`\nBackend switched to ${displayName(backendName)}.`);
86
+ if (previous) {
87
+ console.log(`Previous backend: ${displayName(previous)} ${removed ? '(MCP config removed)' : '(MCP config removal skipped)'}.`);
88
+ }
89
+ }
90
+ process.exit(0);
91
+ }
92
+
43
93
  switch (command) {
44
94
  case "serve": {
45
95
  await startStdioServer(values.device ?? process.env.ZHIHAND_DEVICE);
@@ -55,6 +105,7 @@ switch (command) {
55
105
 
56
106
  case "status": {
57
107
  const cred = loadDefaultCredential();
108
+ const backend = loadBackendConfig();
58
109
  if (cred) {
59
110
  console.log(`Paired device: ${cred.deviceName}`);
60
111
  console.log(`Credential ID: ${cred.credentialId}`);
@@ -63,6 +114,11 @@ switch (command) {
63
114
  } else {
64
115
  console.log("No paired device. Run: zhihand pair");
65
116
  }
117
+ if (backend.activeBackend) {
118
+ console.log(`Active backend: ${displayName(backend.activeBackend)}`);
119
+ } else {
120
+ console.log("No active backend. Run: zhihand claude / zhihand gemini / zhihand codex");
121
+ }
66
122
  break;
67
123
  }
68
124
 
@@ -90,11 +146,26 @@ switch (command) {
90
146
  const tools = await detectCLITools();
91
147
  console.log(formatDetectedTools(tools));
92
148
 
93
- // 3. Setup OpenClaw if present
94
- await detectAndSetupOpenClaw();
149
+ if (tools.length === 0) {
150
+ console.log("\nNo CLI tools detected. Install one of: Claude Code, Codex CLI, Gemini CLI, OpenClaw.");
151
+ break;
152
+ }
153
+
154
+ // 3. Auto-select best tool (logged in + highest priority) and configure
155
+ const best = tools.find((t) => t.loggedIn) ?? tools[0];
156
+ const config = loadBackendConfig();
157
+
158
+ console.log(`\nAuto-selecting backend: ${displayName(best.name)}...`);
159
+
160
+ if (best.name === "openclaw") {
161
+ await detectAndSetupOpenClaw();
162
+ } else {
163
+ configureMCP(best.name, config.activeBackend);
164
+ }
95
165
 
96
- console.log("\nSetup complete. Add to your CLI tool's MCP config:");
97
- console.log(' { "mcpServers": { "zhihand": { "command": "zhihand", "args": ["serve"] } } }');
166
+ saveBackendConfig({ activeBackend: best.name });
167
+ console.log(`\nSetup complete. Backend: ${displayName(best.name)}.`);
168
+ console.log(`To switch backend later: zhihand claude / zhihand gemini / zhihand codex`);
98
169
  break;
99
170
  }
100
171
 
@@ -30,8 +30,10 @@ async function detectGemini() {
30
30
  if (!isCommandAvailable("gemini"))
31
31
  return null;
32
32
  const version = tryExec("gemini --version") ?? "unknown";
33
- // Check login: Google Cloud auth
34
- const loggedIn = tryExec("gemini auth status") !== null;
33
+ // Check login: oauth_creds.json or GOOGLE_API_KEY env var
34
+ const loggedIn = !!process.env.GOOGLE_API_KEY
35
+ || !!process.env.GEMINI_API_KEY
36
+ || tryExec("ls ~/.gemini/oauth_creds.json") !== null;
35
37
  return { name: "gemini", command: "gemini", version, loggedIn, priority: 3 };
36
38
  }
37
39
  async function detectOpenClaw() {
@@ -0,0 +1,9 @@
1
+ import type { BackendName } from "../core/config.ts";
2
+ /**
3
+ * Configure MCP for the selected backend and remove from others.
4
+ */
5
+ export declare function configureMCP(backend: BackendName, previousBackend: BackendName | null): {
6
+ configured: boolean;
7
+ removed: boolean;
8
+ };
9
+ export declare function displayName(backend: BackendName): string;
@@ -0,0 +1,65 @@
1
+ import { execSync } from "node:child_process";
2
+ const MCP_COMMANDS = {
3
+ claudecode: {
4
+ add: "claude mcp add zhihand -- zhihand serve",
5
+ remove: "claude mcp remove zhihand",
6
+ },
7
+ codex: {
8
+ add: "codex mcp add zhihand -- zhihand serve",
9
+ remove: "codex mcp remove zhihand",
10
+ },
11
+ gemini: {
12
+ add: "gemini mcp add --scope user zhihand zhihand -- serve",
13
+ remove: "gemini mcp remove --scope user zhihand",
14
+ },
15
+ };
16
+ const DISPLAY_NAMES = {
17
+ claudecode: "Claude Code",
18
+ codex: "Codex CLI",
19
+ gemini: "Gemini CLI",
20
+ openclaw: "OpenClaw",
21
+ };
22
+ function tryRun(cmd) {
23
+ try {
24
+ execSync(cmd, { stdio: "pipe", timeout: 10_000 });
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ /**
32
+ * Configure MCP for the selected backend and remove from others.
33
+ */
34
+ export function configureMCP(backend, previousBackend) {
35
+ let removed = false;
36
+ let configured = false;
37
+ // Remove from previous backend if different
38
+ if (previousBackend && previousBackend !== backend && previousBackend !== "openclaw") {
39
+ const cmds = MCP_COMMANDS[previousBackend];
40
+ if (cmds) {
41
+ console.log(` Removing MCP config from ${DISPLAY_NAMES[previousBackend]}...`);
42
+ removed = tryRun(cmds.remove);
43
+ }
44
+ }
45
+ // Add to new backend
46
+ if (backend === "openclaw") {
47
+ console.log(` OpenClaw uses plugin system. Run: openclaw plugins install @zhihand/mcp`);
48
+ configured = true;
49
+ }
50
+ else {
51
+ const cmds = MCP_COMMANDS[backend];
52
+ console.log(` Configuring MCP for ${DISPLAY_NAMES[backend]}...`);
53
+ try {
54
+ execSync(cmds.add, { stdio: "inherit", timeout: 10_000 });
55
+ configured = true;
56
+ }
57
+ catch (err) {
58
+ console.error(` Failed to configure ${DISPLAY_NAMES[backend]}: ${err.message}`);
59
+ }
60
+ }
61
+ return { configured, removed };
62
+ }
63
+ export function displayName(backend) {
64
+ return DISPLAY_NAMES[backend];
65
+ }
@@ -1,2 +1,23 @@
1
1
  import type { CLITool } from "./detect.ts";
2
- export declare function spawnCLITask(tool: CLITool, prompt: string): Promise<string>;
2
+ export interface SpawnOptions {
3
+ model?: string;
4
+ timeout?: number;
5
+ }
6
+ /**
7
+ * Spawn a CLI tool interactively, inheriting stdio.
8
+ * Returns the exit code.
9
+ */
10
+ export declare function spawnInteractive(command: string, args: string[], options?: {
11
+ timeout?: number;
12
+ env?: Record<string, string>;
13
+ }): Promise<number>;
14
+ /**
15
+ * Launch a CLI tool with a prompt. For Gemini, uses interactive mode (-i).
16
+ * For others, uses their respective prompt flags.
17
+ */
18
+ export declare function launchCLI(tool: CLITool, prompt: string, options?: SpawnOptions): Promise<number>;
19
+ /**
20
+ * Non-interactive spawn that captures output (for MCP-initiated tasks).
21
+ * Uses spawnSync with argument arrays to avoid shell injection.
22
+ */
23
+ export declare function spawnCLITask(tool: CLITool, prompt: string): string;
package/dist/cli/spawn.js CHANGED
@@ -1,31 +1,96 @@
1
- import { execSync } from "node:child_process";
2
- function shellEscape(s) {
3
- return `'${s.replace(/'/g, "'\\''")}'`;
1
+ import { spawn } from "node:child_process";
2
+ /**
3
+ * Spawn a CLI tool interactively, inheriting stdio.
4
+ * Returns the exit code.
5
+ */
6
+ export function spawnInteractive(command, args, options) {
7
+ return new Promise((resolve, reject) => {
8
+ const child = spawn(command, args, {
9
+ stdio: "inherit",
10
+ env: { ...process.env, ...options?.env },
11
+ });
12
+ const timer = options?.timeout
13
+ ? setTimeout(() => {
14
+ child.kill("SIGTERM");
15
+ reject(new Error(`Process timed out after ${options.timeout}ms`));
16
+ }, options.timeout)
17
+ : undefined;
18
+ child.on("error", (err) => {
19
+ if (timer)
20
+ clearTimeout(timer);
21
+ reject(err);
22
+ });
23
+ child.on("close", (code) => {
24
+ if (timer)
25
+ clearTimeout(timer);
26
+ resolve(code ?? 1);
27
+ });
28
+ });
4
29
  }
5
- export async function spawnCLITask(tool, prompt) {
6
- const escaped = shellEscape(prompt);
30
+ /**
31
+ * Launch a CLI tool with a prompt. For Gemini, uses interactive mode (-i).
32
+ * For others, uses their respective prompt flags.
33
+ */
34
+ export async function launchCLI(tool, prompt, options) {
35
+ const timeout = options?.timeout ?? 300_000;
36
+ switch (tool.name) {
37
+ case "claudecode": {
38
+ const args = ["-p", prompt, "--output-format", "json"];
39
+ return spawnInteractive(tool.command, args, { timeout });
40
+ }
41
+ case "codex": {
42
+ const args = ["-q", prompt, "--json"];
43
+ return spawnInteractive(tool.command, args, { timeout });
44
+ }
45
+ case "gemini": {
46
+ const model = options?.model ?? process.env.CLAUDE_GEMINI_MODEL ?? "gemini-3.1-pro-preview";
47
+ const args = [
48
+ "--approval-mode", "yolo",
49
+ "--model", model,
50
+ "-i", prompt,
51
+ ];
52
+ const env = {
53
+ GEMINI_SANDBOX: "false",
54
+ TERM: process.env.TERM ?? "xterm-256color",
55
+ COLORTERM: process.env.COLORTERM ?? "truecolor",
56
+ };
57
+ return spawnInteractive(tool.command, args, { timeout, env });
58
+ }
59
+ case "openclaw": {
60
+ const args = ["run", prompt];
61
+ return spawnInteractive(tool.command, args, { timeout });
62
+ }
63
+ default:
64
+ throw new Error(`Unsupported CLI tool: ${tool.name}`);
65
+ }
66
+ }
67
+ /**
68
+ * Non-interactive spawn that captures output (for MCP-initiated tasks).
69
+ * Uses spawnSync with argument arrays to avoid shell injection.
70
+ */
71
+ export function spawnCLITask(tool, prompt) {
72
+ const { spawnSync } = require("node:child_process");
73
+ const opts = { encoding: "utf8", timeout: 300_000 };
74
+ let result;
7
75
  switch (tool.name) {
8
76
  case "claudecode":
9
- return execSync(`${tool.command} -p ${escaped} --output-format json`, {
10
- encoding: "utf8",
11
- timeout: 300_000,
12
- });
77
+ result = spawnSync(tool.command, ["-p", prompt, "--output-format", "json"], opts);
78
+ break;
13
79
  case "codex":
14
- return execSync(`${tool.command} -q ${escaped} --json`, {
15
- encoding: "utf8",
16
- timeout: 300_000,
17
- });
80
+ result = spawnSync(tool.command, ["-q", prompt, "--json"], opts);
81
+ break;
18
82
  case "gemini":
19
- return execSync(`${tool.command} -p ${escaped}`, {
20
- encoding: "utf8",
21
- timeout: 300_000,
22
- });
83
+ result = spawnSync(tool.command, ["--approval-mode", "yolo", "-p", prompt], opts);
84
+ break;
23
85
  case "openclaw":
24
- return execSync(`${tool.command} run ${escaped}`, {
25
- encoding: "utf8",
26
- timeout: 300_000,
27
- });
86
+ result = spawnSync(tool.command, ["run", prompt], opts);
87
+ break;
28
88
  default:
29
89
  throw new Error(`Unsupported CLI tool: ${tool.name}`);
30
90
  }
91
+ if (result.error)
92
+ throw result.error;
93
+ if (result.status !== 0)
94
+ throw new Error(result.stderr || `Process exited with code ${result.status}`);
95
+ return result.stdout;
31
96
  }
@@ -16,6 +16,10 @@ export interface ZhiHandConfig {
16
16
  edgeId?: string;
17
17
  timeoutMs?: number;
18
18
  }
19
+ export type BackendName = "claudecode" | "codex" | "gemini" | "openclaw";
20
+ export interface BackendConfig {
21
+ activeBackend: BackendName | null;
22
+ }
19
23
  export declare function resolveZhiHandDir(): string;
20
24
  export declare function ensureZhiHandDir(): void;
21
25
  export declare function loadCredentialStore(): CredentialStore | null;
@@ -24,3 +28,5 @@ export declare function saveCredential(name: string, cred: DeviceCredential, set
24
28
  export declare function resolveConfig(deviceName?: string): ZhiHandConfig;
25
29
  export declare function loadState<T = unknown>(): T | null;
26
30
  export declare function saveState(state: unknown): void;
31
+ export declare function loadBackendConfig(): BackendConfig;
32
+ export declare function saveBackendConfig(config: BackendConfig): void;
@@ -4,11 +4,12 @@ import os from "node:os";
4
4
  const ZHIHAND_DIR = path.join(os.homedir(), ".zhihand");
5
5
  const CREDENTIALS_PATH = path.join(ZHIHAND_DIR, "credentials.json");
6
6
  const STATE_PATH = path.join(ZHIHAND_DIR, "state.json");
7
+ const BACKEND_PATH = path.join(ZHIHAND_DIR, "backend.json");
7
8
  export function resolveZhiHandDir() {
8
9
  return ZHIHAND_DIR;
9
10
  }
10
11
  export function ensureZhiHandDir() {
11
- fs.mkdirSync(ZHIHAND_DIR, { recursive: true });
12
+ fs.mkdirSync(ZHIHAND_DIR, { recursive: true, mode: 0o700 });
12
13
  }
13
14
  export function loadCredentialStore() {
14
15
  if (!fs.existsSync(CREDENTIALS_PATH))
@@ -32,7 +33,7 @@ export function saveCredential(name, cred, setDefault = true) {
32
33
  store.devices[name] = cred;
33
34
  if (setDefault)
34
35
  store.default = name;
35
- fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(store, null, 2));
36
+ fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(store, null, 2), { mode: 0o600 });
36
37
  }
37
38
  export function resolveConfig(deviceName) {
38
39
  const store = loadCredentialStore();
@@ -65,3 +66,17 @@ export function saveState(state) {
65
66
  ensureZhiHandDir();
66
67
  fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
67
68
  }
69
+ export function loadBackendConfig() {
70
+ if (!fs.existsSync(BACKEND_PATH))
71
+ return { activeBackend: null };
72
+ try {
73
+ return JSON.parse(fs.readFileSync(BACKEND_PATH, "utf8"));
74
+ }
75
+ catch {
76
+ return { activeBackend: null };
77
+ }
78
+ }
79
+ export function saveBackendConfig(config) {
80
+ ensureZhiHandDir();
81
+ fs.writeFileSync(BACKEND_PATH, JSON.stringify(config, null, 2));
82
+ }
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
- const PACKAGE_VERSION = "0.12.3";
8
+ const PACKAGE_VERSION = "0.15.0";
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.12.3",
3
+ "version": "0.15.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",