Skip to content
Merged
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

This is an npm workspace: shared code lives in `packages/` (`shared`, `core`, `ui`,
`mcp-app`, `skills`, `native`, `runtimes`) and each shipped product in `apps/` (`codex-plugin`,
`claude`, `mac`). Keep host chrome out of `packages/ui`: chrome every MCP Apps
`claude`, `cursor`, `mac`). Keep host chrome out of `packages/ui`: chrome every MCP Apps
host shares goes in `packages/mcp-app`, chrome for one host in its app. Drive host
differences from advertised capabilities rather than host names. See
[CONTRIBUTING.md](CONTRIBUTING.md#repository-layout).
Expand Down
37 changes: 37 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ packages know no host by name.
| `packages/skills` | Agent skills for the shared tools; each host's package step includes the ones its host supports. |
| `apps/codex-plugin` | Codex: manifest, launcher, assets, and its own server (`server/`, the self-updater) and panel (`ui/`, the update banner). |
| `apps/claude` | Claude: the Claude Desktop extension (`manifest.json`, MCPB) and the Claude Code plugin (`.claude-plugin/plugin.json`). Both run the shared server and panel unchanged. |
| `apps/cursor` | Cursor: the Cursor plugin manifest (`.cursor-plugin/plugin.json`). It runs the shared server and panel unchanged. |
| `apps/mac` | The Mac app: a SwiftUI shell (`Sources/`), the page it hosts (`web/`) and its server (`server/`). |
| `packages/test-support` | Fixtures shared by tests across packages and apps. |
| `tooling` | Shared build, native, release and device smoke-test scripts. Run them from the repository root. |
Expand Down Expand Up @@ -306,6 +307,42 @@ duplicate servers. Rebuild, repackage, and reinstall to update a bundled copy.
Local builds retain the `development` telemetry environment. Public releases use
`npm run build:release` and `npm run package:claude -- --release`.

### Cursor local development

Complete the first-time runtime and native-helper setup under [Build and package](#build-and-package),
then run from this checkout with Node.js 22.19 or later:

```sh
npm run build
npm run cursor:config
```

1. Merge the `mobile-dev` entry from `.local-dev/cursor-mcp.json` into the
`mcpServers` object in `~/.cursor/mcp.json`, preserving other servers. Use the
global file rather than a project's `.cursor/mcp.json`: the snippet uses absolute
paths to this checkout and the Node executable running the generator.
2. Reload Cursor, check that `mobile-dev` is enabled in the MCP section of Cursor Settings, and
ask **“Open Mobile Dev”** in a new agent chat.

After UI or server edits, run `npm run build` and reload Cursor. Regenerate the
config if the checkout or Node path changes.

To create and check a plugin folder for manual installation instead:

```sh
npm run package:cursor
npm run test:cursor
```

This writes the Cursor plugin folder `release/cursor-plugin/mobile-dev` from the shared
runtime. It uses `apps/cursor/.cursor-plugin/plugin.json`, which starts
`${CURSOR_PLUGIN_ROOT}/dist/server.mjs` with Node.js from `PATH`, and leaves out the
Codex manifest, `.mcp.json`, launcher and updater.
Follow the [manual installation steps](README.md#cursor-manual-installation).
Remove the `mobile-dev` development entry before installing the plugin to avoid
duplicate servers. Public releases use `npm run build:release` and
`npm run package:cursor -- --release`.

## Live React UI development

In Codex, run `npm run dev` after installing the plugin. It watches the UI sources and
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ The bundle includes the plugin and native helpers; Claude supplies Node, so no
the bundle or the [local development connection](CONTRIBUTING.md#claude-desktop-local-development)
to avoid duplicate Mobile Dev servers. See [Claude's installation guide](https://support.claude.com/en/articles/10949351-getting-started-with-local-mcp-servers-on-claude-desktop).

### Cursor: manual installation

On an Apple Silicon Mac with Node.js 22.19 or later on `PATH`, obtain the
`mobile-dev` Cursor plugin folder from the person sharing the preview, or
[build one locally](CONTRIBUTING.md#cursor-local-development). The Cursor plugin is
not yet published to the Cursor Marketplace.

1. Copy the `mobile-dev` folder to `~/.cursor/plugins/local/mobile-dev`.
2. Run **Developer: Reload Window**, or restart Cursor, and confirm Mobile Dev
appears under **Customize**.
3. Start a new agent chat and ask **"Open Mobile Dev."** Approve the tool calls
when prompted.

Cursor renders the Mobile Dev views where MCP Apps are available. Otherwise tools
return text, and the agent asks you to choose devices in chat. Copy a newer folder
and reload to update. Use either the plugin or the [local development connection](CONTRIBUTING.md#cursor-local-development)
to avoid duplicate Mobile Dev servers. See [Cursor's plugin guide](https://cursor.com/docs/plugins).

### Mac app

The Mac app shows the devices and tools in their own window, without a chat. Pick
Expand Down Expand Up @@ -112,8 +130,8 @@ See [storage privacy and review checks](docs/storage-privacy.md).

Mobile Dev runs on an Apple Silicon Mac. In Codex desktop, the prebuilt plugin
uses Codex's bundled Node runtime from its workspace dependency cache; the Claude
Desktop bundle uses Claude's Node runtime, and the Mac app bundles its own. Install
the tools for the platforms you use:
Desktop bundle uses Claude's Node runtime, the Cursor plugin uses Node.js from
`PATH`, and the Mac app bundles its own. Install the tools for the platforms you use:

| Platform or feature | Requirements |
| --- | --- |
Expand All @@ -136,7 +154,8 @@ its device panel, inspection, logs, and performance tools.
## Get started

Ask your agent to open the simulator beside this chat. In Codex, you can also
open Mobile Dev from the sidebar. Pick a device from the toolbar; selecting a stopped simulator or AVD
open Mobile Dev from the sidebar; in Cursor, ask **"Open Mobile Dev"** in an agent chat.
Pick a device from the toolbar; selecting a stopped simulator or AVD
boots it. Use the iOS and Android toggles to show either platform or both.

Click an iOS simulator or Android screen to focus it, then press **Command-V** to
Expand Down Expand Up @@ -178,6 +197,7 @@ product in `apps/`:
| `packages/native`, `packages/runtimes` | Native helpers and bundled Node runtimes |
| `apps/codex-plugin` | The Codex plugin: manifest, updater, and update banner |
| `apps/claude` | Claude: the Claude Desktop extension and Claude Code plugin manifests |
| `apps/cursor` | Cursor: the Cursor plugin manifest |
| `apps/mac` | The Mac app: SwiftUI window and its page |

To build a host, install a local package, or contribute changes, see
Expand Down
30 changes: 30 additions & 0 deletions apps/cursor/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "mobile-dev",
"description": "Stream iOS and Android devices into the chat while building, running, or debugging mobile apps. Read native and Metro logs, inspect and control apps, and record CPU, memory, and FPS. Apple Silicon development preview.",
"author": {
"name": "Callstack",
"email": "hello@callstack.com"
},
"homepage": "https://github.com/callstackincubator/mobile-dev",
"repository": "https://github.com/callstackincubator/mobile-dev",
"license": "MIT",
"keywords": [
"ios",
"android",
"simulator",
"emulator",
"react-native",
"expo",
"logs",
"performance"
],
"logo": "assets/icon.svg",
"mcpServers": {
"mobile-dev": {
"command": "node",
"args": [
"${CURSOR_PLUGIN_ROOT}/dist/server.mjs"
]
}
}
}
7 changes: 7 additions & 0 deletions apps/cursor/assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions apps/cursor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@mobile-dev/cursor",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cursor: the Cursor plugin manifest for the shared server and MCP Apps panel."
}
18 changes: 18 additions & 0 deletions apps/cursor/tooling/config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { access, mkdir, writeFile } from "node:fs/promises";
import { resolve } from "node:path";

// Generate a snippet only; never replace the user's existing Cursor configuration.
await access("dist/server.mjs");
const config = {
mcpServers: {
"mobile-dev": {
type: "stdio",
command: process.execPath,
args: [resolve("dist/server.mjs")],
},
},
};
await mkdir(".local-dev", { recursive: true });
const path = resolve(".local-dev/cursor-mcp.json");
await writeFile(path, JSON.stringify(config, null, 2) + "\n");
console.log(`Merge the mcpServers entry from ${path} into ~/.cursor/mcp.json, then reload Cursor.`);
21 changes: 21 additions & 0 deletions apps/cursor/tooling/package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { cp, copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { telemetryBuildEnvironment } from "../../../tooling/telemetry-build.mjs";
import { assertRuntime, releaseVersion, stageRuntime } from "../../../tooling/host-runtime.mjs";

// Cursor runs the shared server and MCP Apps panel unchanged: the host-neutral
// runtime plus its plugin manifest, started with Node from PATH, without Codex files.
const telemetryEnvironment = telemetryBuildEnvironment();
await assertRuntime(telemetryEnvironment);
const version = await releaseVersion();
const documents = ["README.md", "LICENSE", "THIRD_PARTY_NOTICES.md", "PRIVACY.md", "TERMS.md", "SUPPORT.md"];
const { name, ...fields } = JSON.parse(await readFile("apps/cursor/.cursor-plugin/plugin.json", "utf8"));
const plugin = resolve(`release/cursor-plugin/${name}`);
await rm(plugin, { recursive: true, force: true });
await mkdir(`${plugin}/.cursor-plugin`, { recursive: true });
await stageRuntime(`${plugin}/dist`);
await cp("apps/cursor/assets", `${plugin}/assets`, { recursive: true });
for (const skill of ["mobile-dev", "mobile-dev-setup"]) await cp(`packages/skills/${skill}`, `${plugin}/skills/${skill}`, { recursive: true, verbatimSymlinks: true });
for (const path of documents) await copyFile(path, `${plugin}/${path}`);
await writeFile(`${plugin}/.cursor-plugin/plugin.json`, JSON.stringify({ name, version, ...fields }, null, 2) + "\n");
console.log(`Cursor plugin: ${plugin}`);
67 changes: 67 additions & 0 deletions apps/cursor/tooling/smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import assert from "node:assert/strict";
import { access, cp, mkdir, mkdtemp, readFile, 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";
import { releaseVersion } from "../../../tooling/host-runtime.mjs";

const version = await releaseVersion();
const temporary = await mkdtemp(join(tmpdir(), "mobile-dev-cursor-"));
try {
// Copy the plugin to a path with spaces, as ~/.cursor/plugins/local or an install cache would.
const plugin = join(temporary, "plugin with spaces");
await cp(resolve(process.argv[2] ?? "release/cursor-plugin/mobile-dev"), plugin, { recursive: true, verbatimSymlinks: true });
const manifest = JSON.parse(await readFile(join(plugin, ".cursor-plugin/plugin.json"), "utf8"));
assert.equal(manifest.name, "mobile-dev");
assert.equal(manifest.version, version);
assert.equal(manifest.license, "MIT");
await access(join(plugin, manifest.logo));
const server = manifest.mcpServers["mobile-dev"];
assert.equal(server.command, "node");
assert.deepEqual(server.args, ["${CURSOR_PLUGIN_ROOT}/dist/server.mjs"]);
for (const path of ["LICENSE", "README.md", "PRIVACY.md", "skills/mobile-dev/SKILL.md", "skills/mobile-dev-setup/SKILL.md", "dist/app.html", "dist/baguette/Baguette", "dist/ios-mirror/darwin-arm64.node"]) {
await access(join(plugin, path));
}
// Another host's files would load its launcher or manifest.
for (const path of [".mcp.json", "mcp.json", ".codex-plugin", ".claude-plugin", "scripts/launch-mcp.sh"]) {
await assert.rejects(access(join(plugin, path)), path);
}
const home = join(temporary, "home");
await mkdir(home);
// Behavior follows client capabilities, not the host's name: without forms or
// MCP Apps, device choices fall back to chat.
for (const environment of ["development", "release"]) {
const client = new Client({ name: "cursor-plugin-smoke", version: "1" }, { capabilities: {} });
const transport = new StdioClientTransport({
command: process.execPath, args: server.args.map(arg => arg.replaceAll("${CURSOR_PLUGIN_ROOT}", plugin)),
cwd: temporary, stderr: "pipe",
env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin", HOME: home,
MOBILE_DEV_TELEMETRY: "off", MOBILE_DEV_ENVIRONMENT: environment },
});
let diagnostics = "";
transport.stderr?.on("data", chunk => { diagnostics = (diagnostics + chunk).slice(-5000); });
try {
await client.connect(transport);
assert.match(client.getInstructions(), /in chat when it is unsupported/);
const { tools } = await client.listTools();
for (const name of ["mobile_show_devices", "mobile_open_workspace", "mobile_open_simulator", "mobile_list_simulators", "mobile_list_android_devices"]) {
assert.ok(tools.some(tool => tool.name === name), name);
}
const workspace = tools.find(tool => tool.name === "mobile_open_workspace");
const resource = await client.readResource({ uri: workspace._meta.ui.resourceUri });
const html = resource.contents[0].text;
assert.ok(html.includes(`name="mobile-dev-environment" content="${environment}"`));
assert.match(html, /name="mobile-dev-telemetry" content="off"/);
const choice = await client.callTool({ name: "mobile_show_devices", arguments: { message: "Which device?", selectionMode: "single" } });
assert.equal(choice.isError, true);
assert.match(choice.content[0].text, /ask the user in chat/);
// The Codex updater belongs to Codex's server; this plugin runs the shared one.
assert.equal(tools.some(tool => tool.name.includes("plugin_")), false);
console.log(`${environment}: Cursor plugin manifest, payload, MCP startup from the plugin root, chat fallback and no Codex updater 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 }); }
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The Agent Device MCP entry and skill are currently disabled; their implementatio
## Hosts

The server (`packages/core`), the shared UI (`packages/ui`) and the MCP Apps panel
(`packages/mcp-app`) run in every host. Codex and Claude Desktop load the panel as an
(`packages/mcp-app`) run in every host. Codex, Claude and Cursor load the panel as an
MCP App: the `App` from ext-apps carries tool calls and resource reads through the
host, and the panel adapts to the capabilities each host advertises. The Mac app
(`apps/mac`) starts the server over stdio and relays its JSON-RPC lines to a web view,
Expand All @@ -17,7 +17,7 @@ A host adds to the shared pieces without changing them:

- **Server:** each host's entry calls `runStdioServer` with its `ServerFeature`s.
Codex adds the self-updater, which installs through Codex's marketplace; the Mac
app adds its page resource; Claude Desktop runs the shared server unchanged.
app adds its page resource; Claude and Cursor run the shared server unchanged.
- **Panel:** a host's page entry calls `startMcpApp` with its chrome. Codex adds the
update banner; Claude Desktop uses the shared entry.
- **Views:** shared views depend only on `HostBridge`, the subset of `App` they use,
Expand Down
3 changes: 2 additions & 1 deletion docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ to identify the legacy Git marketplace for cleanup. GitHub tags and releases do
not advertise store updates. Successful checks
are cached for one hour across chats in the active Codex profile; unavailable
checks retry after fifteen minutes. Old GitHub update caches are discarded.
Development builds do not check or migrate public installations.
Development builds do not check or migrate public installations. The Claude and
Cursor packages run the shared server, which has no updater.

The app-server plugin API is under development. This integration deliberately
accepts its compatibility risk. If discovery fails or its response no longer
Expand Down
9 changes: 5 additions & 4 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
## Hosts

Since 0.1.165, UI and server telemetry carry a `host` tag and attribute with one of
`codex`, `claude`, `mac` or `other`. Codex, Claude Desktop and other MCP Apps hosts
load the same panel and server; the Mac app runs the same server and shared UI.
`codex`, `claude`, `cursor`, `mac` or `other`. Codex, Claude, Cursor and other MCP
Apps hosts load the same panel and server; the Mac app runs the same server and shared UI.
Behavior never depends on this value; it only attributes measurements and errors.

- The Mac page names itself (`data-host="mac"`); the Mac app also starts the server
with `MOBILE_DEV_HOST=mac`.
- MCP Apps panels are labelled once connected: `codex` when the host advertises
OpenAI extensions, otherwise the host's reported name mapped by `telemetryHost`
(`claude` for Claude, else `other`). Calls before connecting carry no host.
(`claude` for Claude, `cursor` for Cursor, else `other`). Before the Cursor plugin,
Cursor sessions were reported as `other`. Calls before connecting carry no host.
- The server labels itself from the connecting MCP client's name on initialize and
reports `other` until then.
- The server's metadata allowlist accepts only these four values; the raw host or
- The server's metadata allowlist accepts only these values; the raw host or
client name is never sent.

Earlier releases have no `host` value. Surfaces, measurement names, units and
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"packages/test-support",
"apps/codex-plugin",
"apps/claude",
"apps/cursor",
"apps/mac"
],
"type": "module",
Expand Down Expand Up @@ -49,6 +50,9 @@
"claude:config": "node apps/claude/tooling/config.mjs",
"package:claude": "node apps/claude/tooling/package.mjs",
"test:claude": "node apps/claude/tooling/smoke.mjs",
"cursor:config": "node apps/cursor/tooling/config.mjs",
"package:cursor": "node apps/cursor/tooling/package.mjs",
"test:cursor": "node apps/cursor/tooling/smoke.mjs",
"vendor:serve-emu": "node tooling/vendor-serve-emu.mjs",
"dev": "node apps/codex-plugin/tooling/dev-ui.mjs",
"dev:off": "node apps/codex-plugin/tooling/dev-ui.mjs --off",
Expand Down
Loading
Loading