@beeos-ai/device-mcp-server 0.3.0 → 0.4.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.
Files changed (56) hide show
  1. package/dist/backends/android-adb.d.ts +147 -6
  2. package/dist/backends/android-adb.js +776 -40
  3. package/dist/backends/android-adb.js.map +1 -1
  4. package/dist/backends/base.d.ts +243 -7
  5. package/dist/backends/base.js +81 -2
  6. package/dist/backends/base.js.map +1 -1
  7. package/dist/backends/desktop.d.ts +3 -2
  8. package/dist/backends/desktop.js +9 -3
  9. package/dist/backends/desktop.js.map +1 -1
  10. package/dist/backends/linux.js +3 -0
  11. package/dist/backends/linux.js.map +1 -1
  12. package/dist/backends/mac.d.ts +11 -2
  13. package/dist/backends/mac.js +39 -1
  14. package/dist/backends/mac.js.map +1 -1
  15. package/dist/backends/stubs/windows.js +3 -0
  16. package/dist/backends/stubs/windows.js.map +1 -1
  17. package/dist/cli.d.ts +40 -26
  18. package/dist/cli.js +118 -84
  19. package/dist/cli.js.map +1 -1
  20. package/dist/index.d.ts +9 -6
  21. package/dist/index.js +9 -6
  22. package/dist/index.js.map +1 -1
  23. package/dist/server/app.d.ts +60 -17
  24. package/dist/server/app.js +182 -138
  25. package/dist/server/app.js.map +1 -1
  26. package/dist/server/mcp-server.d.ts +25 -0
  27. package/dist/server/mcp-server.js +33 -0
  28. package/dist/server/mcp-server.js.map +1 -0
  29. package/dist/server/registry.d.ts +111 -0
  30. package/dist/server/registry.js +191 -0
  31. package/dist/server/registry.js.map +1 -0
  32. package/dist/server/stdio.d.ts +29 -0
  33. package/dist/server/stdio.js +35 -0
  34. package/dist/server/stdio.js.map +1 -0
  35. package/dist/server/tool-registry.d.ts +60 -35
  36. package/dist/server/tool-registry.js +911 -434
  37. package/dist/server/tool-registry.js.map +1 -1
  38. package/dist/util/adb-files.d.ts +25 -1
  39. package/dist/util/adb-files.js +95 -0
  40. package/dist/util/adb-files.js.map +1 -1
  41. package/dist/util/locale.d.ts +16 -0
  42. package/dist/util/locale.js +31 -0
  43. package/dist/util/locale.js.map +1 -0
  44. package/dist/util/logger.d.ts +27 -0
  45. package/dist/util/logger.js +27 -0
  46. package/dist/util/logger.js.map +1 -0
  47. package/dist/util/output-path.d.ts +60 -0
  48. package/dist/util/output-path.js +123 -0
  49. package/dist/util/output-path.js.map +1 -0
  50. package/dist/util/package-name.d.ts +26 -0
  51. package/dist/util/package-name.js +41 -0
  52. package/dist/util/package-name.js.map +1 -0
  53. package/package.json +5 -3
  54. package/dist/server/action-mapping.d.ts +0 -21
  55. package/dist/server/action-mapping.js +0 -153
  56. package/dist/server/action-mapping.js.map +0 -1
@@ -0,0 +1,191 @@
1
+ /**
2
+ * BackendRegistry — manages multiple `SandboxBackend` instances inside a
3
+ * **single** device-mcp-server process.
4
+ *
5
+ * Replaces the legacy "one process per device" topology (mirrored from the
6
+ * `mobile-mcp` design): a single `device-mcp-server` discovers every adb device
7
+ * in the host's `adb devices` output and lazily wraps each one in an
8
+ * `AndroidAdbBackend`. Tool handlers resolve the right backend through three
9
+ * fallbacks (in priority order):
10
+ *
11
+ * 1. explicit `device` argument on the tool call (`device: "emulator-5554"`)
12
+ * 2. session-sticky default set via the `use_device` MCP tool (keyed by
13
+ * `extra.sessionId`, only meaningful for stdio / single-session clients)
14
+ * 3. single-device shortcut: if exactly one device is connected, use it
15
+ *
16
+ * If none of the above resolves to a backend the registry throws
17
+ * `DeviceError("device required", subtype:"invalid_args")` so the MCP tool
18
+ * call returns a structured error instead of silently picking a random device.
19
+ *
20
+ * The registry also owns a periodic `adb devices` poll (default 10s) so newly
21
+ * plugged devices show up in `list_available_devices` without restarting the
22
+ * process. Connect-state validation per backend is lazy — backends are
23
+ * constructed with `skipConnectValidation: true` and only emit errors at
24
+ * tool-call time.
25
+ *
26
+ * Desktop / stub backends are registered up front (one each) and re-used
27
+ * across every session — the device id for a desktop backend is its
28
+ * `os` string (`"desktop-macos"`, `"desktop-linux"`, ...).
29
+ */
30
+ import { DeviceError } from "@beeos-ai/device-common";
31
+ import { listAdbDevices, AndroidAdbBackend } from "../backends/android-adb.js";
32
+ export class BackendRegistry {
33
+ backends = new Map();
34
+ enableAdbDiscovery;
35
+ adbRunner;
36
+ pollIntervalMs;
37
+ androidDefaults;
38
+ pollTimer;
39
+ /**
40
+ * Per-session active-device map (only meaningful for stdio / single-session
41
+ * transports). For stateless HTTP `extra.sessionId` is undefined and this
42
+ * map is never written.
43
+ */
44
+ sessionDevice = new Map();
45
+ constructor(opts = {}) {
46
+ this.enableAdbDiscovery = opts.enableAdbDiscovery ?? true;
47
+ this.adbRunner = opts.adbRunner;
48
+ this.pollIntervalMs = opts.pollIntervalMs ?? 10_000;
49
+ this.androidDefaults = opts.androidDefaults;
50
+ for (const b of opts.initialBackends ?? []) {
51
+ // Static (desktop / stub) backends — id == os.
52
+ this.backends.set(b.os, b);
53
+ }
54
+ for (const serial of opts.initialAdbSerials ?? []) {
55
+ this.backends.set(serial, this.makeAdbBackend(serial));
56
+ }
57
+ }
58
+ /**
59
+ * Kick off the adb-discovery poll and run one immediate scan. Returns
60
+ * after the first scan completes (so callers can `await` and immediately
61
+ * see a populated `list()`).
62
+ *
63
+ * Idempotent — safe to call multiple times.
64
+ */
65
+ async start() {
66
+ if (!this.enableAdbDiscovery)
67
+ return;
68
+ await this.pollOnce().catch(() => {
69
+ /* swallow — next poll will surface errors as device disappearance */
70
+ });
71
+ if (this.pollTimer)
72
+ return;
73
+ this.pollTimer = setInterval(() => {
74
+ void this.pollOnce().catch(() => undefined);
75
+ }, this.pollIntervalMs);
76
+ // Don't keep the event loop alive for the poll alone.
77
+ this.pollTimer.unref?.();
78
+ }
79
+ /** Stop the discovery poll and disconnect every backend. */
80
+ async stop() {
81
+ if (this.pollTimer) {
82
+ clearInterval(this.pollTimer);
83
+ this.pollTimer = undefined;
84
+ }
85
+ await Promise.all([...this.backends.values()].map((b) => b.disconnect().catch(() => undefined)));
86
+ }
87
+ /** Snapshot of every currently-known device. */
88
+ list() {
89
+ const ids = [...this.backends.keys()];
90
+ const isSingle = ids.length === 1;
91
+ return ids.map((id) => {
92
+ const b = this.backends.get(id);
93
+ return {
94
+ id,
95
+ os: b.os,
96
+ isDefault: isSingle,
97
+ source: b.os === "android" ? "adb" : "desktop",
98
+ };
99
+ });
100
+ }
101
+ /**
102
+ * Resolve a backend for a single tool call. `args.device` wins; if absent,
103
+ * fall back to the session-sticky `use_device` value; if also absent and
104
+ * exactly one backend is registered, use it. Otherwise throw with a clear
105
+ * "device required" error so the MCP client knows it must call
106
+ * `list_available_devices` / `use_device` first.
107
+ */
108
+ resolve(args, sessionId) {
109
+ const explicit = typeof args?.device === "string" ? args.device : undefined;
110
+ if (explicit) {
111
+ const b = this.backends.get(explicit);
112
+ if (!b) {
113
+ throw new DeviceError(`device '${explicit}' is not connected; call list_available_devices`, { subtype: "invalid_args", retriable: false });
114
+ }
115
+ return b;
116
+ }
117
+ if (sessionId) {
118
+ const sticky = this.sessionDevice.get(sessionId);
119
+ if (sticky) {
120
+ const b = this.backends.get(sticky);
121
+ if (b)
122
+ return b;
123
+ // sticky reference is stale — drop it so the caller gets the
124
+ // "device required" message instead of a stale-id error.
125
+ this.sessionDevice.delete(sessionId);
126
+ }
127
+ }
128
+ if (this.backends.size === 1) {
129
+ return this.backends.values().next().value;
130
+ }
131
+ if (this.backends.size === 0) {
132
+ throw new DeviceError("no devices connected; plug in a device or call list_available_devices", { subtype: "no_device", retriable: true });
133
+ }
134
+ throw new DeviceError("multiple devices connected; pass `device: <id>` or call use_device first", { subtype: "invalid_args", retriable: false });
135
+ }
136
+ /** Set the session-sticky default device. Used by the `use_device` tool. */
137
+ useDevice(sessionId, deviceId) {
138
+ if (!this.backends.has(deviceId)) {
139
+ throw new DeviceError(`device '${deviceId}' is not connected; call list_available_devices`, { subtype: "invalid_args", retriable: false });
140
+ }
141
+ if (sessionId) {
142
+ this.sessionDevice.set(sessionId, deviceId);
143
+ }
144
+ else {
145
+ // No session = HTTP stateless. Treat as a no-op + structured error so
146
+ // clients learn the "always pass `device`" rule explicitly.
147
+ throw new DeviceError("use_device has no effect over stateless HTTP — pass `device` on every tool call instead", { subtype: "invalid_args", retriable: false });
148
+ }
149
+ }
150
+ async pollOnce() {
151
+ let serials = [];
152
+ try {
153
+ serials = await listAdbDevices(this.adbRunner);
154
+ }
155
+ catch {
156
+ // adb unavailable on this host — nothing to add. Static (desktop)
157
+ // backends seeded at construction time stay registered.
158
+ return;
159
+ }
160
+ const seen = new Set(serials);
161
+ for (const serial of serials) {
162
+ if (!this.backends.has(serial)) {
163
+ this.backends.set(serial, this.makeAdbBackend(serial));
164
+ }
165
+ }
166
+ // Prune adb backends whose devices have disappeared. Static (desktop)
167
+ // backends are never adb serials so they're untouched by this sweep.
168
+ for (const [id, backend] of this.backends.entries()) {
169
+ if (backend.os === "android" && !seen.has(id)) {
170
+ this.backends.delete(id);
171
+ backend.disconnect().catch(() => undefined);
172
+ // Drop session-sticky pointers to the removed device.
173
+ for (const [sid, dev] of this.sessionDevice.entries()) {
174
+ if (dev === id)
175
+ this.sessionDevice.delete(sid);
176
+ }
177
+ }
178
+ }
179
+ }
180
+ makeAdbBackend(serial) {
181
+ return new AndroidAdbBackend({
182
+ ...this.androidDefaults,
183
+ serial,
184
+ // Lazy connect — discovery already proved the device is in `adb
185
+ // devices`, so a per-backend `adb devices` validation is redundant
186
+ // and would add startup latency proportional to device count.
187
+ skipConnectValidation: true,
188
+ });
189
+ }
190
+ }
191
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/server/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Stdio transport for `device-mcp-server`.
3
+ *
4
+ * Used when the server is launched as a child process by an MCP client
5
+ * (Cursor, Claude Desktop, custom orchestrators). Single persistent
6
+ * connection over stdin/stdout — `extra.sessionId` is stable for the
7
+ * lifetime of the process so `use_device` / `list_available_devices`
8
+ * give a `mobile-mcp`-style stateful UX.
9
+ *
10
+ * IMPORTANT: nothing may write to stdout in stdio mode — that channel
11
+ * is reserved for JSON-RPC frames. All logging goes through `stderr`
12
+ * via `util/logger`. The `runStdio()` function intentionally does NOT
13
+ * `console.log` or `process.stdout.write` anything.
14
+ */
15
+ import type { BackendRegistry } from "./registry.js";
16
+ import { type Logger } from "../util/logger.js";
17
+ export interface RunStdioOptions {
18
+ registry: BackendRegistry;
19
+ name?: string;
20
+ version?: string;
21
+ logger?: Logger;
22
+ }
23
+ /**
24
+ * Connect a fresh `McpServer` to stdin/stdout. Resolves once the
25
+ * transport is wired up — the process keeps running because the SDK
26
+ * holds the stdin readable open. Callers should let `process.exit`
27
+ * happen naturally on stdin EOF.
28
+ */
29
+ export declare function runStdio(opts: RunStdioOptions): Promise<void>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Stdio transport for `device-mcp-server`.
3
+ *
4
+ * Used when the server is launched as a child process by an MCP client
5
+ * (Cursor, Claude Desktop, custom orchestrators). Single persistent
6
+ * connection over stdin/stdout — `extra.sessionId` is stable for the
7
+ * lifetime of the process so `use_device` / `list_available_devices`
8
+ * give a `mobile-mcp`-style stateful UX.
9
+ *
10
+ * IMPORTANT: nothing may write to stdout in stdio mode — that channel
11
+ * is reserved for JSON-RPC frames. All logging goes through `stderr`
12
+ * via `util/logger`. The `runStdio()` function intentionally does NOT
13
+ * `console.log` or `process.stdout.write` anything.
14
+ */
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import { buildMcpServer } from "./mcp-server.js";
17
+ import { stderrLogger } from "../util/logger.js";
18
+ /**
19
+ * Connect a fresh `McpServer` to stdin/stdout. Resolves once the
20
+ * transport is wired up — the process keeps running because the SDK
21
+ * holds the stdin readable open. Callers should let `process.exit`
22
+ * happen naturally on stdin EOF.
23
+ */
24
+ export async function runStdio(opts) {
25
+ const log = opts.logger ?? stderrLogger;
26
+ const server = buildMcpServer({
27
+ registry: opts.registry,
28
+ name: opts.name,
29
+ version: opts.version,
30
+ });
31
+ const transport = new StdioServerTransport();
32
+ await server.connect(transport);
33
+ log.info?.({ name: opts.name ?? "device-mcp-server", version: opts.version }, "stdio_ready");
34
+ }
35
+ //# sourceMappingURL=stdio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../src/server/stdio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAe,MAAM,mBAAmB,CAAC;AAS9D;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAqB;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,IAAI,YAAY,CAAC;IACxC,MAAM,MAAM,GAAG,cAAc,CAAC;QAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC,CAAC;IACH,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,GAAG,CAAC,IAAI,EAAE,CACR,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,mBAAmB,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,EACjE,aAAa,CACd,CAAC;AACJ,CAAC"}
@@ -1,50 +1,75 @@
1
1
  /**
2
- * MCP tool registry verbs exposed by the device sandbox.
2
+ * SDK-style tool catalogue for device-mcp-server.
3
3
  *
4
- * Each tool entry carries:
5
- * - a name (the MCP tool identifier, e.g. `click`)
6
- * - a JSON schema describing the expected `arguments` payload
7
- * - a `handler` that maps `arguments` `SandboxBackend` method call.
4
+ * Each tool is a `ToolSpec` describing:
5
+ * - `name` MCP tool identifier (`screenshot`, `tap`, ...)
6
+ * - `description` human-readable verb
7
+ * - `inputShape` zod raw shape every shape gets an implicit
8
+ * `device?: z.string().optional()` injected by
9
+ * `registerAllTools` so multi-device callers can
10
+ * target a specific backend per call
11
+ * - `annotations` `ToolAnnotations` (read-only / destructive /
12
+ * idempotent / open-world hints)
13
+ * - `requires` MCP tool name the resolved backend must declare
14
+ * in `backend.tools`. Tools with `requires:
15
+ * "registry"` are registry-level (don't touch a
16
+ * backend) — used for `list_available_devices` /
17
+ * `use_device`.
18
+ * - `handler` async `(args, ctx) => CallToolResult`
8
19
  *
9
- * Filtering is driven entirely by `backend.tools` (a flat
10
- * `ReadonlySet<string>` declared on each backend). The registry is
11
- * purely a name schema/handler lookup table; it does not encode
12
- * support gates of its own. `listToolDescriptorsFor(backend)` filters
13
- * via `backend.tools.has(name)`; `getToolFor(name, backend)` mirrors
14
- * the same gate so `/mcp/tools/call` returns 404 (instead of 200 +
15
- * runtime `unsupported`) for any tool the active backend doesn't
16
- * advertise.
20
+ * `registerAllTools(server, registry)` iterates the catalogue and calls
21
+ * `server.registerTool` for each entry. The handler closure captures the
22
+ * registry so it can resolve the per-call backend via `registry.resolve`.
17
23
  *
18
- * Inspired by the Python `mcp_tools/registry.py` reference; verbs are kept
19
- * verbatim for cross-runtime compatibility.
24
+ * Returning shapes:
25
+ * - Every handler returns `{ content, structuredContent }`. `content`
26
+ * carries the human-friendly text summary (printable in MCP clients
27
+ * like Claude Desktop / Cursor); `structuredContent` carries the
28
+ * typed payload that programmatic callers (`device-agent`) parse.
20
29
  */
21
- import type { ToolDescriptor } from "@beeos-ai/device-common";
30
+ import { z } from "zod";
31
+ import type { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
32
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
33
  import type { SandboxBackend } from "../backends/base.js";
34
+ import type { BackendRegistry } from "./registry.js";
35
+ /** Backend reference passed into handlers for backend-bound tools. */
23
36
  export interface ToolHandlerContext {
24
37
  backend: SandboxBackend;
38
+ registry: BackendRegistry;
39
+ sessionId?: string;
25
40
  }
26
- export type ToolHandler = (args: Record<string, unknown>, ctx: ToolHandlerContext) => Promise<unknown>;
27
- export interface ToolEntry {
28
- descriptor: ToolDescriptor;
41
+ export type ToolHandler = (args: Record<string, unknown>, ctx: ToolHandlerContext) => Promise<CallToolResult>;
42
+ export interface ToolSpec {
43
+ name: string;
44
+ description: string;
45
+ /** Zod raw shape (NO `device` — the registrar injects it automatically). */
46
+ inputShape: z.ZodRawShape;
47
+ annotations?: ToolAnnotations;
48
+ /**
49
+ * Backend tool gate. Either an MCP tool name (must appear in
50
+ * `backend.tools`) or `"registry"` for registry-level tools that don't
51
+ * need a backend at all.
52
+ */
53
+ requires: string | "registry";
29
54
  handler: ToolHandler;
30
55
  }
31
- export declare const TOOLS: Record<string, ToolEntry>;
56
+ export declare const TOOL_SPECS: ToolSpec[];
32
57
  /**
33
- * Return ALL registered tool descriptors regardless of backend support.
34
- *
35
- * Useful for documentation / tests that want to enumerate the catalog.
36
- * Wire callers SHOULD use `listToolDescriptorsFor(backend)` so the list
37
- * only contains tools the active backend can actually execute.
58
+ * Look up a tool spec by name. Used by tests / introspection — production
59
+ * code goes through `registerAllTools(server, registry)`.
38
60
  */
39
- export declare function listToolDescriptors(): ToolDescriptor[];
40
- /** Return tool descriptors filtered by the backend's advertised tool set. */
41
- export declare function listToolDescriptorsFor(backend: SandboxBackend): ToolDescriptor[];
42
- /** Look up a tool by name, ignoring backend tool-set gates. */
43
- export declare function getTool(name: string): ToolEntry | undefined;
61
+ export declare function getToolSpec(name: string): ToolSpec | undefined;
62
+ /** Ordered list of every tool name advertised by the server. */
63
+ export declare function allToolNames(): string[];
44
64
  /**
45
- * Look up a tool by name, **gated** on the backend's advertised tool
46
- * set. Returns `undefined` for tools the active backend cannot
47
- * satisfy the `/mcp/tools/call` route uses this and returns 404,
48
- * mirroring the filtering applied at `/mcp/tools/list`.
65
+ * Register every tool spec onto the given `McpServer`. Each handler closure
66
+ * captures the registry so per-call backend resolution stays inside the
67
+ * tool layer (the SDK itself is backend-agnostic).
68
+ *
69
+ * Backend-bound tools are gated on the resolved backend's `tools` set —
70
+ * if the active backend doesn't advertise the tool we throw a
71
+ * `DeviceError("unsupported")` so the SDK returns a structured error to
72
+ * the client. Registry-level tools (`list_available_devices`,
73
+ * `use_device`) skip this gate.
49
74
  */
50
- export declare function getToolFor(name: string, backend: SandboxBackend): ToolEntry | undefined;
75
+ export declare function registerAllTools(server: McpServer, registry: BackendRegistry): void;