Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

[Back to README](../README.md) · [Contributing](../CONTRIBUTING.md)

## Standard MCP Apps hosts

Explicit host profiles preserve the existing UI/server/native initialization,
release environment, identity, sampling and scrubbing. Standard context and chat
handoffs retain their existing timing and handled-error boundaries. Workspace
display-mode changes use `ui.workspace.display_mode_changed`; resize failures use
`workspace.resize`. Surface/device attribution and hidden-panel cleanup remain
on the active rendering and streaming paths. The Codex updater and its polling
are disabled outside Codex. No native telemetry changes are required.

VS Code prepares messages in the composer without submitting a turn. Its success
counters are `ui.annotations.prepare_success`, `ui.logs.prepare`,
`ui.recording.ask_prepared`, `ui.recording.open_prepared`,
`ui.comparison.ask_prepared`, `ui.tracesift.investigate_prepared` and
`ui.tracesift.pick_investigate_prepared`. Existing send/action counters retain
their behavior on other hosts. Message-acknowledgement timings still measure the
host round trip; they do not measure model response time or prove submission.

## Volume policy

Since 0.1.164, application metrics in the UI, Node servers and native helpers use
Expand Down
76 changes: 76 additions & 0 deletions docs/vscode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# VS Code / GitHub Copilot development preview

Runs the shared Mobile Dev MCP server and native helpers on an Apple Silicon
Mac. Requires Node >=22.19, Xcode for iOS and the Android SDK for Android.
Use a current VS Code with GitHub Copilot and MCP Apps enabled
(`chat.mcp.apps.enabled`). This preview has automated protocol/package coverage;
actual VS Code rendering, device streaming and input still need validation.

## Run from source

Complete repository setup, then:

```sh
npm run build
npm run vscode:config
```

Use **MCP: Open User Configuration** and merge the `servers` entry from
`.local-dev/vscode-mcp.json`. Preserve existing servers. Alternatively use your
app project's `.vscode/mcp.json`. For SSH/dev-container work, keep this stdio
server in the **local** user profile so it can reach the Mac's Xcode/ADB/devices.
The generated Node and server paths are absolute. Restart the MCP server after
rebuilding; regenerate configuration after moving the checkout or Node.

Ask: **Open Mobile Dev and list available devices without booting anything.**
Choose ambiguous targets in chat or in the device picker.

## Relocatable bundle

```sh
npm run package:vscode
npm run test:vscode
```

Unzip `release/mobile-dev-<version>-vscode-darwin-arm64.zip` into a persistent
directory, run `node configure.mjs` there, then merge the printed configuration
into VS Code. It uses the Node running that command and requires no Codex
installation. Replace the bundle and restart the server to update. Codex update
checks and installation are disabled. This is an MCP bundle, not a VSIX.

Local builds retain development telemetry. Public packaging requires
`npm run build:release` and `npm run package:vscode -- --release`, plus the normal
source-map and native-symbol upload workflow.

## VS Code behavior

- The workspace runs inline at a reported bounded height, with responsive
device/tools layout. It declares only inline mode. A docked editor panel or
fullscreen workspace would require separate VS Code extension work.
- **Prepare question** / **Prepare investigation** fills the chat input. Review
the text and press **Send** to start. Logs, screenshot context and annotations
remain selected until explicitly cleared; a prepared question is not a sent
turn. Questions include their selected log/recording details.
- The explicit VS Code profile accommodates versions that handle `ui/message`
but omit `message.text`, and context updates without a `text` subcapability.
Other hosts retain capability checks. Rejections/timeouts are shown without
automatically retrying an uncertain handoff. Existing copy actions remain.

## Validation before declaring support

- Open an existing iOS simulator, stream for five minutes, tap and drag.
- Confirm the inline height, keyboard/menu behavior and device/tool selectors.
- Prepare a log/annotation/recording question, verify it does not submit, then
submit manually and check the agent receives the correct device and reference.
- Check image attachments and explicit clearing, including a manually removed
composer attachment. Standard context provides no composer readback; local
selections remain app-owned and can be published again on a later update.
- Hide/reopen cards, switch chats and verify collectors and observers clean up.
- Validate Android H.264 and physical-iOS HEVC independently of simulator JPEG.

The smoke test relocates the archive into a path with spaces and verifies MCP
startup, UI resources, telemetry opt-out and update isolation without booting
devices. Existing stream/input telemetry remains; prepared-message outcomes use
separate counters described in `docs/telemetry.md`.

Source: [VS Code MCP developer guide](https://code.visualstudio.com/api/extension-guides/ai/mcp).
5 changes: 5 additions & 0 deletions integrations/vscode/INSTALL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Run `node configure.mjs` from this extracted bundle. Merge the printed `servers`
entry into the local user-profile MCP configuration in VS Code. The generated
configuration uses absolute paths and does not modify any editor settings.

See README.md for prerequisites, update instructions and preview limitations.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"test:ios-cpu": "node scripts/smoke-ios-cpu.mjs",
"package": "node scripts/package.mjs",
"package:release": "node scripts/package.mjs --release",
"vscode:config": "node scripts/vscode-config.mjs",
"package:vscode": "node scripts/package-vscode.mjs",
"test:vscode": "node scripts/smoke-editor-package.mjs vscode",
"vendor:serve-emu": "node scripts/vendor-serve-emu.mjs",
"dev": "node scripts/dev-ui.mjs",
"dev:off": "node scripts/dev-ui.mjs --off",
Expand Down
12 changes: 12 additions & 0 deletions scripts/local-host-config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { access } from "node:fs/promises";
import { resolve } from "node:path";

// Generate data only. The caller decides where to save it; user settings are
// never overwritten. Resolve paths now so GUI hosts need no shell PATH or cwd.
export async function localHostConfig(host, root = ".", node = process.execPath) {
if (host !== "cursor" && host !== "vscode") throw new Error("Unsupported editor host.");
const server = resolve(root, "dist/server.mjs");
await access(server);
const entry = { type: "stdio", command: node, args: [server], env: { MOBILE_DEV_HOST: host } };
return host === "vscode" ? { servers: { "mobile-dev": entry } } : { mcpServers: { "mobile-dev": entry } };
}
34 changes: 34 additions & 0 deletions scripts/package-editor.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { execFileSync } from "node:child_process";
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { telemetryBuildEnvironment } from "./telemetry-build.mjs";

export async function packageEditor(host) {
if (host !== "cursor" && host !== "vscode") throw new Error("Unsupported editor host.");
telemetryBuildEnvironment();
// Retain the canonical native inventory, licenses and environment checks.
execFileSync(process.execPath, ["scripts/package.mjs", ...process.argv.slice(2)], { stdio: "inherit" });
const { version } = JSON.parse(await readFile("package.json", "utf8"));
const source = resolve("release/marketplace/plugins/mobile-dev");
const directory = resolve(`release/${host}/mobile-dev`);
await rm(directory, { recursive: true, force: true });
await mkdir(directory, { recursive: true });
for (const path of ["dist", "THIRD_PARTY_NOTICES.md", "PRIVACY.md", "TERMS.md", "SUPPORT.md"]) {
await cp(`${source}/${path}`, `${directory}/${path}`, { recursive: true, verbatimSymlinks: true });
}
await cp(`integrations/${host}`, directory, { recursive: true });
await cp(`docs/${host}.md`, `${directory}/README.md`);
await cp("scripts/local-host-config.mjs", `${directory}/local-host-config.mjs`);
await writeFile(`${directory}/configure.mjs`, `import { localHostConfig } from "./local-host-config.mjs";\nconsole.log(JSON.stringify(await localHostConfig(${JSON.stringify(host)}, import.meta.dirname), null, 2));\n`);
if (host === "cursor") {
const path = `${directory}/.cursor-plugin/plugin.json`;
const manifest = JSON.parse(await readFile(path, "utf8"));
manifest.version = version;
await writeFile(path, JSON.stringify(manifest, null, 2) + "\n");
}
const archive = resolve(`release/mobile-dev-${version}-${host}-darwin-arm64.zip`);
await rm(archive, { force: true });
execFileSync("zip", ["-qr", archive, "."], { cwd: directory });
execFileSync("unzip", ["-tq", archive], { stdio: "inherit" });
console.log(`${host} bundle: ${archive}`);
}
2 changes: 2 additions & 0 deletions scripts/package-vscode.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { packageEditor } from "./package-editor.mjs";
await packageEditor("vscode");
60 changes: 60 additions & 0 deletions scripts/smoke-editor-package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { access, mkdir, mkdtemp, readFile, realpath, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const host = process.argv[2];
if (host !== "cursor" && host !== "vscode") throw new Error("Pass cursor or vscode.");
const { version } = JSON.parse(await readFile("package.json", "utf8"));
const archive = resolve(process.argv[3] ?? `release/mobile-dev-${version}-${host}-darwin-arm64.zip`);
const temporary = await realpath(await mkdtemp(join(tmpdir(), `mobile-dev-${host}-`)));
try {
const directory = join(temporary, "bundle with spaces");
await mkdir(directory);
execFileSync("unzip", ["-q", archive, "-d", directory]);
const config = JSON.parse(execFileSync(process.execPath, [join(directory, "configure.mjs")], { cwd: temporary, encoding: "utf8" }));
const entry = config[host === "cursor" ? "mcpServers" : "servers"]["mobile-dev"];
assert.equal(entry.command, process.execPath);
assert.equal(entry.env.MOBILE_DEV_HOST, host);
assert.equal(entry.args[0], join(directory, "dist/server.mjs"));
if (host === "cursor") {
const manifest = JSON.parse(await readFile(join(directory, ".cursor-plugin/plugin.json"), "utf8"));
assert.equal(manifest.version, version);
const pluginConfig = JSON.parse(await readFile(join(directory, "mcp.json"), "utf8"));
assert.equal(pluginConfig.mcpServers["mobile-dev"].env.MOBILE_DEV_HOST, host);
assert.equal(pluginConfig.mcpServers["mobile-dev"].args[0].replaceAll("${CURSOR_PLUGIN_ROOT}", directory), entry.args[0]);
}
for (const path of ["dist/app.html", "dist/baguette/Baguette", "dist/ios-mirror/darwin-arm64.node", "dist/serve-emu/src/cli.mjs"]) await access(join(directory, path));
const home = join(temporary, "home");
await mkdir(home);
for (const environment of ["development", "release"]) {
const client = new Client({ name: `${host}-package-smoke`, version: "1" });
const transport = new StdioClientTransport({
command: entry.command, args: entry.args, cwd: temporary, stderr: "pipe",
env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin", HOME: home, ...entry.env, MOBILE_DEV_TELEMETRY: "off", MOBILE_DEV_ENVIRONMENT: environment },
});
let diagnostics = "";
transport.stderr?.on("data", chunk => { diagnostics = (diagnostics + chunk).slice(-4000); });
try {
await client.connect(transport);
const { tools } = await client.listTools();
assert.ok(!tools.some(tool => tool.name === "mobile_choose_devices"));
const workspace = tools.find(tool => tool.name === "mobile_open_workspace");
assert.ok(workspace);
const resource = await client.readResource({ uri: workspace._meta.ui.resourceUri });
const html = resource.contents[0].text;
assert.ok(html.includes(`data-host="${host}"`));
assert.ok(html.includes(`name="mobile-dev-environment" content="${environment}"`));
assert.ok(html.includes('name="mobile-dev-telemetry" content="off"'));
const check = await client.callTool({ name: "mobile_check_plugin_update", arguments: {} });
assert.equal(check.structuredContent.update.status, "disabled");
const install = await client.callTool({ name: "mobile_install_plugin_update", arguments: {} });
assert.equal(install.isError, true);
console.log(`${host}/${environment}: relocated startup, UI resource, telemetry opt-out and updater isolation passed.`);
} catch (error) { if (diagnostics) console.error(diagnostics); throw error; }
finally { await client.close(); await transport.close(); }
}
} finally { await rm(temporary, { recursive: true, force: true }); }
9 changes: 9 additions & 0 deletions scripts/vscode-config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { mkdir, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { localHostConfig } from "./local-host-config.mjs";

const config = await localHostConfig("vscode");
await mkdir(".local-dev", { recursive: true });
const path = resolve(".local-dev/vscode-mcp.json");
await writeFile(path, JSON.stringify(config, null, 2) + "\n");
console.log(`Merge the mobile-dev entry from ${path} into VS Code's local user-profile mcp.json or your project's .vscode/mcp.json, preserving existing servers.`);
Loading
Loading