@zhihand/mcp 0.33.1 → 0.34.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 CHANGED
@@ -21,6 +21,8 @@ import {
21
21
  DEFAULT_MODELS,
22
22
  resolveConfig,
23
23
  resolveDefaultEndpoint,
24
+ loadPluginIdentity,
25
+ clearPluginIdentity,
24
26
  } from "../dist/core/config.js";
25
27
  import {
26
28
  executePairingNewUser,
@@ -207,6 +209,24 @@ switch (command) {
207
209
  break;
208
210
  }
209
211
 
212
+ case "identity": {
213
+ const subCmd = positionals[1];
214
+ if (subCmd === "reset") {
215
+ clearPluginIdentity();
216
+ console.log("Plugin identity cleared. Next 'zhihand pair' will register a new identity.");
217
+ } else {
218
+ const id = loadPluginIdentity();
219
+ if (id) {
220
+ console.log(`Edge ID: ${id.edge_id}`);
221
+ console.log(`Stable Identity: ${id.stable_identity}`);
222
+ console.log(`Plugin Secret: ${id.plugin_secret.slice(0, 6)}...(redacted)`);
223
+ } else {
224
+ console.log("No plugin identity found. Run 'zhihand pair' to create one.");
225
+ }
226
+ }
227
+ break;
228
+ }
229
+
210
230
  case "mcp":
211
231
  case "serve": {
212
232
  await startStdioServer();
@@ -34,9 +34,20 @@ export interface BackendConfig {
34
34
  model?: string | null;
35
35
  }
36
36
  export declare const DEFAULT_MODELS: Record<Exclude<BackendName, "openclaw">, string>;
37
+ export interface PluginIdentity {
38
+ stable_identity: string;
39
+ edge_id: string;
40
+ plugin_secret: string;
41
+ }
37
42
  export declare function resolveZhiHandDir(): string;
38
43
  export declare function ensureZhiHandDir(): void;
39
44
  export declare function getConfigPath(): string;
45
+ /** Read persisted Plugin identity. Returns null if missing or malformed. */
46
+ export declare function loadPluginIdentity(): PluginIdentity | null;
47
+ /** Atomically persist Plugin identity (write-to-tmp + rename, mode 0o600). */
48
+ export declare function savePluginIdentity(identity: PluginIdentity): void;
49
+ /** Delete identity.json (used by `zhihand identity reset`). */
50
+ export declare function clearPluginIdentity(): void;
40
51
  export declare function loadConfig(): ZhihandConfigV3;
41
52
  /**
42
53
  * Atomically write config: write to .tmp, then rename. Prevents corruption
@@ -11,6 +11,7 @@ const ZHIHAND_DIR = path.join(os.homedir(), ".zhihand");
11
11
  const CONFIG_PATH = path.join(ZHIHAND_DIR, "config.json");
12
12
  const STATE_PATH = path.join(ZHIHAND_DIR, "state.json");
13
13
  const BACKEND_PATH = path.join(ZHIHAND_DIR, "backend.json");
14
+ const IDENTITY_PATH = path.join(ZHIHAND_DIR, "identity.json");
14
15
  export function resolveZhiHandDir() {
15
16
  return ZHIHAND_DIR;
16
17
  }
@@ -20,6 +21,36 @@ export function ensureZhiHandDir() {
20
21
  export function getConfigPath() {
21
22
  return CONFIG_PATH;
22
23
  }
24
+ // ── Plugin identity I/O ──────────────────────────────────
25
+ /** Read persisted Plugin identity. Returns null if missing or malformed. */
26
+ export function loadPluginIdentity() {
27
+ try {
28
+ const raw = JSON.parse(fs.readFileSync(IDENTITY_PATH, "utf-8"));
29
+ if (raw.stable_identity && raw.edge_id && raw.plugin_secret) {
30
+ return raw;
31
+ }
32
+ return null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ /** Atomically persist Plugin identity (write-to-tmp + rename, mode 0o600). */
39
+ export function savePluginIdentity(identity) {
40
+ ensureZhiHandDir();
41
+ const tmp = IDENTITY_PATH + ".tmp";
42
+ fs.writeFileSync(tmp, JSON.stringify(identity, null, 2), { mode: 0o600 });
43
+ fs.renameSync(tmp, IDENTITY_PATH);
44
+ }
45
+ /** Delete identity.json (used by `zhihand identity reset`). */
46
+ export function clearPluginIdentity() {
47
+ try {
48
+ fs.unlinkSync(IDENTITY_PATH);
49
+ }
50
+ catch {
51
+ // already gone
52
+ }
53
+ }
23
54
  // ── v3 config I/O ──────────────────────────────────────────
24
55
  let legacyWarningPrinted = false;
25
56
  function emptyConfig() {
@@ -1,4 +1,4 @@
1
- import type { DeviceRecord, UserRecord } from "./config.ts";
1
+ import type { DeviceRecord, UserRecord, PluginIdentity } from "./config.ts";
2
2
  export interface PairingSession {
3
3
  session_id: string;
4
4
  pair_url: string;
@@ -30,7 +30,14 @@ export declare function registerPlugin(endpoint: string, options: {
30
30
  adapterKind?: string;
31
31
  }): Promise<{
32
32
  edge_id: string;
33
+ plugin_secret: string;
33
34
  }>;
35
+ /**
36
+ * Ensure a persistent Plugin identity exists. Reuses existing identity
37
+ * if present; otherwise registers a new plugin and persists the result.
38
+ * All pair operations share the same identity → same EdgeID.
39
+ */
40
+ export declare function ensurePluginIdentity(endpoint: string): Promise<PluginIdentity>;
34
41
  /**
35
42
  * Poll pairing session until claimed or expired.
36
43
  */
package/dist/core/pair.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import QRCode from "qrcode";
2
- import { addUser, addDeviceToUser, ensureZhiHandDir, saveState, resolveDefaultEndpoint, getUserRecord, cleanupLegacyConfig, } from "./config.js";
2
+ import os from "node:os";
3
+ import { addUser, addDeviceToUser, ensureZhiHandDir, saveState, resolveDefaultEndpoint, getUserRecord, cleanupLegacyConfig, loadPluginIdentity, savePluginIdentity, } from "./config.js";
3
4
  import { fetchDeviceProfileOnce, extractStatic } from "./device.js";
4
5
  import { fetchUserCredentials } from "./ws.js";
5
6
  // ── Server API helpers ─────────────────────────────────────
@@ -58,7 +59,24 @@ export async function registerPlugin(endpoint, options) {
58
59
  throw new Error(`Register plugin failed: ${response.status} ${await response.text()}`);
59
60
  }
60
61
  const payload = (await response.json());
61
- return { edge_id: payload.plugin.edge_id };
62
+ return { edge_id: payload.plugin.edge_id, plugin_secret: payload.plugin.plugin_secret };
63
+ }
64
+ /**
65
+ * Ensure a persistent Plugin identity exists. Reuses existing identity
66
+ * if present; otherwise registers a new plugin and persists the result.
67
+ * All pair operations share the same identity → same EdgeID.
68
+ */
69
+ export async function ensurePluginIdentity(endpoint) {
70
+ const existing = loadPluginIdentity();
71
+ const stableId = existing?.stable_identity ?? `mcp-${os.hostname()}-${Date.now().toString(36)}`;
72
+ const plugin = await registerPlugin(endpoint, { stableIdentity: stableId });
73
+ const identity = {
74
+ stable_identity: stableId,
75
+ edge_id: plugin.edge_id,
76
+ plugin_secret: plugin.plugin_secret,
77
+ };
78
+ savePluginIdentity(identity);
79
+ return identity;
62
80
  }
63
81
  /**
64
82
  * Poll pairing session until claimed or expired.
@@ -97,10 +115,9 @@ export async function executePairingNewUser(preferredLabel) {
97
115
  cleanupLegacyConfig();
98
116
  const userId = userResp.user_id;
99
117
  const controllerToken = userResp.controller_token;
100
- // 2. Register plugin (get edge_id)
101
- const stableIdentity = `mcp-${Date.now().toString(36)}`;
102
- const plugin = await registerPlugin(endpoint, { stableIdentity });
103
- const edgeId = plugin.edge_id;
118
+ // 2. Ensure persistent plugin identity (same EdgeID across all pairs)
119
+ const identity = await ensurePluginIdentity(endpoint);
120
+ const edgeId = identity.edge_id;
104
121
  // 3. Create pairing session
105
122
  const session = await createPairingSession(endpoint, userId, controllerToken, edgeId, 300);
106
123
  saveState({
@@ -186,10 +203,9 @@ export async function executePairingAddDevice(userId, preferredLabel) {
186
203
  if (!user)
187
204
  throw new Error(`User '${userId}' not found in config`);
188
205
  const controllerToken = user.controller_token;
189
- // Register plugin (get edge_id)
190
- const stableIdentity = `mcp-${Date.now().toString(36)}`;
191
- const plugin = await registerPlugin(endpoint, { stableIdentity });
192
- const edgeId = plugin.edge_id;
206
+ // Ensure persistent plugin identity (same EdgeID across all pairs)
207
+ const identity = await ensurePluginIdentity(endpoint);
208
+ const edgeId = identity.edge_id;
193
209
  // Get existing credential IDs before pairing
194
210
  const existingCreds = await fetchUserCredentials(endpoint, userId, controllerToken);
195
211
  const existingIds = new Set(existingCreds.map((c) => c.credential_id));
@@ -1,12 +1,17 @@
1
- import type { ZhiHandRuntimeConfig } from "../core/config.ts";
2
1
  /** Brain metadata included in every heartbeat, so the app always knows the current backend/model. */
3
2
  export interface BrainMeta {
4
3
  backend?: string | null;
5
4
  model?: string | null;
6
5
  }
6
+ /** Plugin-level heartbeat target. Uses edgeId + pluginSecret instead of per-credential auth. */
7
+ export interface HeartbeatTarget {
8
+ controlPlaneEndpoint: string;
9
+ edgeId: string;
10
+ pluginSecret: string;
11
+ }
7
12
  /** Update the backend/model metadata that will be sent with the next heartbeat. */
8
13
  export declare function setBrainMeta(meta: BrainMeta): void;
9
- export declare function sendBrainOnline(config: ZhiHandRuntimeConfig): Promise<boolean>;
10
- export declare function sendBrainOffline(config: ZhiHandRuntimeConfig): Promise<boolean>;
11
- export declare function startHeartbeatLoop(config: ZhiHandRuntimeConfig, log: (msg: string) => void): void;
14
+ export declare function sendBrainOnline(target: HeartbeatTarget): Promise<boolean>;
15
+ export declare function sendBrainOffline(target: HeartbeatTarget): Promise<boolean>;
16
+ export declare function startHeartbeatLoop(target: HeartbeatTarget, log: (msg: string) => void): void;
12
17
  export declare function stopHeartbeatLoop(): void;
@@ -9,23 +9,23 @@ let currentMeta = {};
9
9
  export function setBrainMeta(meta) {
10
10
  currentMeta = meta;
11
11
  }
12
- function buildUrl(config) {
13
- return `${config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(config.credentialId)}/brain-status`;
12
+ function buildUrl(target) {
13
+ return `${target.controlPlaneEndpoint}/v1/plugins/${encodeURIComponent(target.edgeId)}/brain-status`;
14
14
  }
15
- async function sendHeartbeat(config, online) {
15
+ async function sendHeartbeat(target, online) {
16
16
  try {
17
17
  const body = { plugin_online: online };
18
18
  if (currentMeta.backend)
19
19
  body.backend = currentMeta.backend;
20
20
  if (currentMeta.model)
21
21
  body.model = currentMeta.model;
22
- const url = buildUrl(config);
22
+ const url = buildUrl(target);
23
23
  dbg(`[heartbeat] POST ${url} body=${JSON.stringify(body)}`);
24
24
  const response = await fetch(url, {
25
25
  method: "POST",
26
26
  headers: {
27
27
  "Content-Type": "application/json",
28
- "Authorization": `Bearer ${config.controllerToken}`,
28
+ "Authorization": `Bearer ${target.pluginSecret}`,
29
29
  },
30
30
  body: JSON.stringify(body),
31
31
  signal: AbortSignal.timeout(10_000),
@@ -38,20 +38,20 @@ async function sendHeartbeat(config, online) {
38
38
  return false;
39
39
  }
40
40
  }
41
- export async function sendBrainOnline(config) {
42
- return sendHeartbeat(config, true);
41
+ export async function sendBrainOnline(target) {
42
+ return sendHeartbeat(target, true);
43
43
  }
44
- export async function sendBrainOffline(config) {
45
- return sendHeartbeat(config, false);
44
+ export async function sendBrainOffline(target) {
45
+ return sendHeartbeat(target, false);
46
46
  }
47
- export function startHeartbeatLoop(config, log) {
47
+ export function startHeartbeatLoop(target, log) {
48
48
  let retrying = false;
49
49
  stopped = false;
50
50
  async function beat() {
51
51
  // Skip main-timer beats while retry loop is active (avoids overlap & flapping)
52
52
  if (retrying || stopped)
53
53
  return;
54
- const ok = await sendBrainOnline(config);
54
+ const ok = await sendBrainOnline(target);
55
55
  if (stopped)
56
56
  return; // check after await — stopHeartbeatLoop() may have been called
57
57
  if (ok) {
@@ -70,7 +70,7 @@ export function startHeartbeatLoop(config, log) {
70
70
  retryTimer = setTimeout(async () => {
71
71
  if (!retrying || stopped)
72
72
  return;
73
- const recovered = await sendBrainOnline(config);
73
+ const recovered = await sendBrainOnline(target);
74
74
  if (stopped)
75
75
  return; // check after await
76
76
  if (recovered) {
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
6
  // Transport type used only for cleanup interface
7
7
  import { createServer as createMcpServer } from "../index.js";
8
- import { resolveConfig, loadBackendConfig, saveBackendConfig, resolveZhiHandDir, ensureZhiHandDir, DEFAULT_MODELS, } from "../core/config.js";
8
+ import { resolveConfig, loadBackendConfig, saveBackendConfig, resolveZhiHandDir, resolveDefaultEndpoint, ensureZhiHandDir, loadPluginIdentity, DEFAULT_MODELS, } from "../core/config.js";
9
9
  import { PACKAGE_VERSION } from "../index.js";
10
10
  import { startHeartbeatLoop, stopHeartbeatLoop, sendBrainOffline, setBrainMeta } from "./heartbeat.js";
11
11
  import { PromptListener } from "./prompt-listener.js";
@@ -359,15 +359,35 @@ export async function startDaemon(options) {
359
359
  httpServer.listen(port, "127.0.0.1", () => resolve());
360
360
  });
361
361
  writePid();
362
- // Start heartbeat
363
- startHeartbeatLoop(config, log);
364
- // Start prompt listener
365
- const promptListener = new PromptListener(config, (prompt) => onPromptReceived(config, prompt), log);
362
+ // Load Plugin identity for edge-level heartbeat + prompt WS
363
+ const identity = loadPluginIdentity();
364
+ if (!identity) {
365
+ log("[identity] No plugin identity found. Run 'zhihand pair' first.");
366
+ process.exit(1);
367
+ }
368
+ log(`[identity] Loaded: edge_id=${identity.edge_id}, stable_identity=${identity.stable_identity}`);
369
+ const heartbeatTarget = {
370
+ controlPlaneEndpoint: resolveDefaultEndpoint(),
371
+ edgeId: identity.edge_id,
372
+ pluginSecret: identity.plugin_secret,
373
+ };
374
+ // Start heartbeat (edge-level, pluginSecret auth)
375
+ startHeartbeatLoop(heartbeatTarget, log);
376
+ // Start prompt listener (edge-level single WS)
377
+ const promptListener = new PromptListener({
378
+ controlPlaneEndpoint: resolveDefaultEndpoint(),
379
+ edgeId: identity.edge_id,
380
+ pluginSecret: identity.plugin_secret,
381
+ }, (prompt) => onPromptReceived(config, prompt), log, (reason) => {
382
+ log(`[fatal] ${reason}`);
383
+ process.exit(1);
384
+ });
366
385
  promptListener.start();
367
386
  log(`ZhiHand daemon started.`);
368
387
  log(` PID: ${process.pid}`);
369
388
  log(` MCP: http://127.0.0.1:${port}/mcp`);
370
389
  log(` Backend: ${activeBackend ?? "(none)"}`);
390
+ log(` Edge: ${identity.edge_id}`);
371
391
  log(` Device: ${config.credentialId}`);
372
392
  log(`Listening for prompts...`);
373
393
  // Graceful shutdown
@@ -377,7 +397,7 @@ export async function startDaemon(options) {
377
397
  stopHeartbeatLoop();
378
398
  clearInterval(sessionCleanupTimer);
379
399
  await killActiveChild();
380
- await sendBrainOffline(config);
400
+ await sendBrainOffline(heartbeatTarget);
381
401
  // Close all MCP sessions
382
402
  for (const session of mcpSessions.values()) {
383
403
  try {
@@ -1,5 +1,3 @@
1
- import type { ZhiHandRuntimeConfig } from "../core/config.ts";
2
- type ZhiHandConfig = ZhiHandRuntimeConfig;
3
1
  export interface MobilePrompt {
4
2
  id: string;
5
3
  credential_id: string;
@@ -11,14 +9,21 @@ export interface MobilePrompt {
11
9
  attachments?: unknown[];
12
10
  }
13
11
  export type PromptHandler = (prompt: MobilePrompt) => void;
12
+ /** Edge-level prompt listener config. Uses pluginSecret instead of per-user controllerToken. */
13
+ export interface PromptListenerConfig {
14
+ controlPlaneEndpoint: string;
15
+ edgeId: string;
16
+ pluginSecret: string;
17
+ }
14
18
  export declare class PromptListener {
15
19
  private config;
16
20
  private handler;
17
21
  private log;
22
+ private onFatalError?;
18
23
  private processedIds;
19
24
  private rws;
20
25
  private stopped;
21
- constructor(config: ZhiHandConfig, handler: PromptHandler, log: (msg: string) => void);
26
+ constructor(config: PromptListenerConfig, handler: PromptHandler, log: (msg: string) => void, onFatalError?: (reason: string) => void);
22
27
  start(): void;
23
28
  stop(): void;
24
29
  private dispatchPrompt;
@@ -26,4 +31,3 @@ export declare class PromptListener {
26
31
  private handleWSMessage;
27
32
  private handleEvent;
28
33
  }
29
- export {};
@@ -4,13 +4,15 @@ export class PromptListener {
4
4
  config;
5
5
  handler;
6
6
  log;
7
+ onFatalError;
7
8
  processedIds = new Set();
8
9
  rws = null;
9
10
  stopped = false;
10
- constructor(config, handler, log) {
11
+ constructor(config, handler, log, onFatalError) {
11
12
  this.config = config;
12
13
  this.handler = handler;
13
14
  this.log = log;
15
+ this.onFatalError = onFatalError;
14
16
  }
15
17
  start() {
16
18
  this.stopped = false;
@@ -27,7 +29,7 @@ export class PromptListener {
27
29
  return;
28
30
  }
29
31
  this.processedIds.add(prompt.id);
30
- dbg(`[prompt] Dispatching prompt: id=${prompt.id}, status=${prompt.status}, text="${prompt.text.slice(0, 100)}${prompt.text.length > 100 ? "..." : ""}"`);
32
+ dbg(`[prompt] Dispatching prompt: id=${prompt.id}, cred=${prompt.credential_id}, status=${prompt.status}, text="${prompt.text.slice(0, 100)}${prompt.text.length > 100 ? "..." : ""}"`);
31
33
  // Prevent unbounded growth
32
34
  if (this.processedIds.size > 500) {
33
35
  const arr = [...this.processedIds];
@@ -38,21 +40,18 @@ export class PromptListener {
38
40
  connectWS() {
39
41
  if (this.stopped)
40
42
  return;
41
- const wsUrl = `${this.config.controlPlaneEndpoint.replace(/^http/, "ws")}/v1/credentials/${encodeURIComponent(this.config.credentialId)}/ws?topic=prompts`;
43
+ // Edge-level WS: single connection for all credentials under this edge
44
+ const wsUrl = `${this.config.controlPlaneEndpoint.replace(/^http/, "ws")}/v1/plugins/${encodeURIComponent(this.config.edgeId)}/ws?topic=prompts`;
42
45
  dbg(`[ws] Connecting to ${wsUrl}`);
43
46
  this.rws = new ReconnectingWebSocket({
44
47
  url: wsUrl,
45
- headers: {
46
- "Authorization": `Bearer ${this.config.controllerToken}`,
47
- },
48
+ headers: {},
48
49
  onOpen: () => {
49
- // Send auth message as first frame (required by server).
50
+ // Send auth frame with pluginSecret (server verifies before streaming)
50
51
  this.rws.send(JSON.stringify({
51
52
  type: "auth",
52
- controller_token: this.config.controllerToken,
53
- topics: ["prompts"],
53
+ plugin_secret: this.config.pluginSecret,
54
54
  }));
55
- // onConnected deferred until auth_ok is received (see handleWSMessage)
56
55
  },
57
56
  onClose: (_code, _reason) => {
58
57
  dbg("[ws] Disconnected. ReconnectingWebSocket will retry.");
@@ -68,42 +67,38 @@ export class PromptListener {
68
67
  }
69
68
  handleWSMessage(data) {
70
69
  const msg = data;
71
- // Auth responses
72
70
  if (msg.type === "auth_ok") {
73
- this.log("[ws] Connected to prompt stream.");
71
+ this.log("[ws] Connected to Edge prompt stream.");
74
72
  return;
75
73
  }
76
74
  if (msg.type === "auth_error") {
77
- this.log(`[ws] Auth failed: ${msg.error}`);
75
+ const reason = `Plugin auth failed: ${msg.error}. Run 'zhihand pair' to re-register.`;
76
+ this.log(`[ws] ${reason}`);
78
77
  this.rws?.stop();
79
78
  this.rws = null;
79
+ this.onFatalError?.(reason);
80
80
  return;
81
81
  }
82
- // Application-level ping (if server sends these alongside protocol pings)
83
82
  if (msg.type === "ping") {
84
83
  this.rws?.send(JSON.stringify({ type: "pong" }));
85
84
  return;
86
85
  }
87
- // Event dispatch
88
86
  if (msg.type === "event" || msg.kind) {
89
87
  this.handleEvent(msg);
90
88
  }
91
89
  }
92
90
  handleEvent(event) {
93
91
  const kind = event.kind;
94
- dbg(`[ws] Event: kind=${kind}, prompt=${event.prompt?.id ?? "-"}, prompts=${event.prompts?.length ?? 0}`);
92
+ dbg(`[ws] Event: kind=${kind}, prompt=${event.prompt?.id ?? "-"}`);
95
93
  if (kind === "prompt.queued" && event.prompt) {
96
94
  this.dispatchPrompt(event.prompt);
97
95
  }
98
- else if (kind === "prompt.snapshot" && event.prompts) {
99
- for (const p of event.prompts) {
100
- if (p.status === "pending" || p.status === "processing") {
101
- this.dispatchPrompt(p);
102
- }
96
+ else if (kind === "prompt.snapshot" && event.prompt) {
97
+ // Edge WS sends individual snapshot events (one per pending prompt)
98
+ const p = event.prompt;
99
+ if (p.status === "pending" || p.status === "processing") {
100
+ this.dispatchPrompt(p);
103
101
  }
104
102
  }
105
- else if (kind === "device_profile.updated") {
106
- this.log("[device] device_profile.updated event received on prompts stream (ignored; registry handles it)");
107
- }
108
103
  }
109
104
  }
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.33.1";
2
+ export declare const PACKAGE_VERSION = "0.34.0";
3
3
  export declare function createServer(): McpServer;
4
4
  export declare function startStdioServer(): Promise<void>;
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { handlePair } from "./tools/pair.js";
8
8
  import { resolveTargetDevice } from "./tools/resolve.js";
9
9
  import { buildControlToolDescription, buildSystemToolDescription, buildScreenshotToolDescription, formatDeviceStatus, extractDynamic, } from "./core/device.js";
10
10
  import { registry } from "./core/registry.js";
11
- export const PACKAGE_VERSION = "0.33.1";
11
+ export const PACKAGE_VERSION = "0.34.0";
12
12
  function errorResult(message) {
13
13
  return { content: [{ type: "text", text: message }], isError: true };
14
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhihand/mcp",
3
- "version": "0.33.1",
3
+ "version": "0.34.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",