@zhihand/mcp 0.26.0 → 0.26.2

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.
@@ -18,7 +18,6 @@ export interface ControlParams {
18
18
  appPackage?: string;
19
19
  bundleId?: string;
20
20
  urlScheme?: string;
21
- appName?: string;
22
21
  }
23
22
  export interface QueuedControlCommand {
24
23
  type: string;
@@ -1,3 +1,4 @@
1
+ import { getStaticContext, isDeviceProfileLoaded } from "./device.js";
1
2
  import { dbg } from "../daemon/logger.js";
2
3
  let messageCounter = 0;
3
4
  function nextMessageId() {
@@ -54,17 +55,34 @@ export function createControlCommand(params) {
54
55
  };
55
56
  case "open_app": {
56
57
  const appPayload = {};
57
- if (params.appPackage)
58
- appPayload.app_package = params.appPackage;
59
- if (params.bundleId)
60
- appPayload.bundle_id = params.bundleId;
61
- if (params.urlScheme)
62
- appPayload.url_scheme = params.urlScheme;
63
- if (params.appName)
64
- appPayload.app_name = params.appName;
58
+ const platform = isDeviceProfileLoaded() ? getStaticContext().platform : "unknown";
59
+ // Only send platform-appropriate fields — Android strict JSON rejects unknown keys
60
+ if (platform === "android") {
61
+ // Android: only app_package
62
+ if (params.appPackage)
63
+ appPayload.app_package = params.appPackage;
64
+ }
65
+ else if (platform === "ios") {
66
+ // iOS: bundleId or urlScheme
67
+ if (params.bundleId)
68
+ appPayload.bundle_id = params.bundleId;
69
+ if (params.urlScheme)
70
+ appPayload.url_scheme = params.urlScheme;
71
+ }
72
+ else {
73
+ // Unknown platform: send only what's provided, prefer appPackage
74
+ if (params.appPackage)
75
+ appPayload.app_package = params.appPackage;
76
+ else if (params.bundleId)
77
+ appPayload.bundle_id = params.bundleId;
78
+ else if (params.urlScheme)
79
+ appPayload.url_scheme = params.urlScheme;
80
+ }
81
+ // Never send app_name — phone strict JSON parser rejects unknown keys
65
82
  if (!appPayload.app_package && !appPayload.bundle_id && !appPayload.url_scheme) {
66
83
  throw new Error("open_app requires at least one of: appPackage, bundleId, urlScheme");
67
84
  }
85
+ dbg(`[cmd] open_app: platform=${platform}, payload=${JSON.stringify(appPayload)}`);
68
86
  return { type: "receive_app", payload: appPayload };
69
87
  }
70
88
  case "screenshot":
@@ -40,7 +40,7 @@ export declare function getDynamicContext(): DynamicContext;
40
40
  export declare function isDeviceProfileLoaded(): boolean;
41
41
  export declare function extractStatic(profile: Record<string, unknown>): StaticContext;
42
42
  export declare function extractDynamic(profile: Record<string, unknown>): DynamicContext;
43
- export declare function updateDeviceProfile(profile: Record<string, unknown>): void;
43
+ export declare function updateDeviceProfile(raw: Record<string, unknown>): void;
44
44
  export declare function fetchDeviceProfile(config: ZhiHandConfig): Promise<void>;
45
45
  export declare function buildControlToolDescription(): string;
46
46
  export declare function buildScreenshotToolDescription(): string;
@@ -57,16 +57,37 @@ function bool(v, fallback) {
57
57
  return typeof v === "boolean" ? v : fallback;
58
58
  }
59
59
  export function extractStatic(profile) {
60
+ // Build OS version string from platform + system_release + api_level
61
+ const platform = str(profile.platform, DEFAULT_STATIC.platform);
62
+ const sysRelease = str(profile.system_release, "");
63
+ const apiLevel = typeof profile.api_level === "number" ? profile.api_level : null;
64
+ let osVersion;
65
+ if (platform === "android" && sysRelease && apiLevel) {
66
+ osVersion = `Android ${sysRelease} (API ${apiLevel})`;
67
+ }
68
+ else if (platform === "ios" && sysRelease) {
69
+ osVersion = `iOS ${sysRelease}`;
70
+ }
71
+ else {
72
+ osVersion = sysRelease || DEFAULT_STATIC.osVersion;
73
+ }
74
+ // Screen size: Android uses display_width_px/display_height_px, iOS uses display_width_pixels/display_height_pixels
75
+ const screenW = num(profile.display_width_px, num(profile.display_width_pixels, DEFAULT_STATIC.screenWidthPx));
76
+ const screenH = num(profile.display_height_px, num(profile.display_height_pixels, DEFAULT_STATIC.screenHeightPx));
77
+ // Density: Android uses density, iOS uses display_scale
78
+ const density = num(profile.density, num(profile.display_scale, DEFAULT_STATIC.density));
79
+ // Text direction: rtl is boolean
80
+ const textDirection = profile.rtl === true ? "rtl" : "ltr";
60
81
  return {
61
- platform: str(profile.platform, DEFAULT_STATIC.platform),
82
+ platform,
62
83
  model: str(profile.model, DEFAULT_STATIC.model),
63
- osVersion: str(profile.os_version, DEFAULT_STATIC.osVersion),
64
- screenWidthPx: num(profile.screen_width_px, DEFAULT_STATIC.screenWidthPx),
65
- screenHeightPx: num(profile.screen_height_px, DEFAULT_STATIC.screenHeightPx),
66
- density: num(profile.density, DEFAULT_STATIC.density),
84
+ osVersion,
85
+ screenWidthPx: screenW,
86
+ screenHeightPx: screenH,
87
+ density,
67
88
  formFactor: str(profile.form_factor, DEFAULT_STATIC.formFactor),
68
89
  locale: str(profile.locale, DEFAULT_STATIC.locale),
69
- textDirection: str(profile.text_direction, DEFAULT_STATIC.textDirection),
90
+ textDirection,
70
91
  timezone: str(profile.timezone, DEFAULT_STATIC.timezone),
71
92
  navigationMode: typeof profile.navigation_mode === "string" ? profile.navigation_mode : undefined,
72
93
  romFamily: typeof profile.rom_family === "string" ? profile.rom_family : undefined,
@@ -88,7 +109,16 @@ export function extractDynamic(profile) {
88
109
  };
89
110
  }
90
111
  // ── Update from SSE event ─────────────────────────────────
91
- export function updateDeviceProfile(profile) {
112
+ export function updateDeviceProfile(raw) {
113
+ // SSE events may also wrap in { platform, attributes: {...} } — flatten if needed
114
+ let profile;
115
+ if (typeof raw.attributes === "object" && raw.attributes !== null) {
116
+ const attrs = raw.attributes;
117
+ profile = { ...attrs, platform: raw.platform ?? attrs.platform };
118
+ }
119
+ else {
120
+ profile = raw;
121
+ }
92
122
  staticCtx = extractStatic(profile);
93
123
  dynamicCtx = extractDynamic(profile);
94
124
  loaded = true;
@@ -108,10 +138,16 @@ export async function fetchDeviceProfile(config) {
108
138
  return;
109
139
  }
110
140
  const data = await response.json();
111
- // API may wrap in { device_profile: {...} } or return flat
112
- const profile = (typeof data.device_profile === "object" && data.device_profile !== null)
113
- ? data.device_profile
141
+ // API returns { profile: { credential_id, platform, attributes: {...} } }
142
+ const wrapper = (typeof data.profile === "object" && data.profile !== null)
143
+ ? data.profile
114
144
  : data;
145
+ // Merge top-level fields (platform, edge_id) with attributes for flat extraction
146
+ const attrs = (typeof wrapper.attributes === "object" && wrapper.attributes !== null)
147
+ ? wrapper.attributes
148
+ : {};
149
+ const profile = { ...attrs, platform: wrapper.platform ?? attrs.platform };
150
+ dbg(`[device] Raw profile keys: ${Object.keys(profile).join(", ")}`);
115
151
  updateDeviceProfile(profile);
116
152
  }
117
153
  catch (err) {
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.26.0";
2
+ export declare const PACKAGE_VERSION = "0.26.2";
3
3
  export declare function createServer(deviceName?: string): McpServer;
4
4
  export declare function startStdioServer(deviceName?: string): Promise<void>;
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { executeControl } from "./tools/control.js";
6
6
  import { handleScreenshot } from "./tools/screenshot.js";
7
7
  import { handlePair } from "./tools/pair.js";
8
8
  import { getStaticContext, getDynamicContext, fetchDeviceProfile, buildControlToolDescription, buildScreenshotToolDescription, formatDeviceStatus, } from "./core/device.js";
9
- export const PACKAGE_VERSION = "0.26.0";
9
+ export const PACKAGE_VERSION = "0.26.2";
10
10
  export function createServer(deviceName) {
11
11
  const server = new McpServer({
12
12
  name: "zhihand",
@@ -16,7 +16,6 @@ export declare const controlSchema: {
16
16
  appPackage: z.ZodOptional<z.ZodString>;
17
17
  bundleId: z.ZodOptional<z.ZodString>;
18
18
  urlScheme: z.ZodOptional<z.ZodString>;
19
- appName: z.ZodOptional<z.ZodString>;
20
19
  };
21
20
  export declare const screenshotSchema: {};
22
21
  export declare const pairSchema: {
@@ -22,7 +22,6 @@ export const controlSchema = {
22
22
  appPackage: z.string().optional().describe("Android package name, e.g. 'com.tencent.mm'"),
23
23
  bundleId: z.string().optional().describe("iOS bundle ID, e.g. 'com.tencent.xin'"),
24
24
  urlScheme: z.string().optional().describe("URL scheme, e.g. 'weixin://'"),
25
- appName: z.string().optional().describe("Human-readable app name (for logging)"),
26
25
  };
27
26
  export const screenshotSchema = {};
28
27
  export const pairSchema = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhihand/mcp",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
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",