Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 3 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"fixed": [
["@lynx-js/devtool-connector", "@lynx-js/devtool-mcp-server", "@lynx-js/skill-lynx-devtool"]
],
"linked": [],
"access": "public",
"baseBranch": "main",
Expand Down
93 changes: 93 additions & 0 deletions .github/workflows/lynx-devtool.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: Lynx DevTool E2E

on:
pull_request:
branches: [main]
paths:
- "packages/mcp-servers/devtool-connector/**"
- "packages/mcp-servers/devtool-mcp-server/**"
- "packages/skills/lynx-devtool/**"
- ".github/workflows/lynx-devtool.yml"
push:
branches: [main]
paths:
- "packages/mcp-servers/devtool-connector/**"
- "packages/mcp-servers/devtool-mcp-server/**"
- "packages/skills/lynx-devtool/**"
- ".github/workflows/lynx-devtool.yml"
workflow_dispatch:

env:
EMBEDDED_LYNX_TARBALL: https://github.com/lynx-community/skills/releases/download/embedded-lynx-202606041609/embedded-lynx-linux-x86_64.tar.gz
LYNX_DEVTOOL_MCP_TESTING_TRANSPORTS: EmbeddedLynx
LYNX_DEVTOOL_MCP_TESTING_APP_PACKAGE: EmbeddedLynx
LYNX_DEVTOOL_MCP_TESTING_PAGE_URL: https://lynxjs.org/lynx-examples/swiper/dist/Swiper.lynx.bundle
LYNX_DEVTOOL_MCP_TESTING_OPEN_URL: https://lynxjs.org/lynx-examples/swiper/dist/Swiper.lynx.bundle

jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Install Pnpm
run: npm i -g corepack@latest --force && corepack enable

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: "pnpm"

- name: Install Dependencies
run: pnpm install

- name: Start EmbeddedLynx runtime
run: |
set -euo pipefail
curl -sL "$EMBEDDED_LYNX_TARBALL" | tar -xzf - -C /tmp
BINARY="/tmp/embedded-lynx-linux-x86_64/embedded_lynx"
chmod +x "$BINARY"
# Launch the headless Lynx runtime in its own session so it survives
# the end of this step. It loads the page and exposes a debug-router
# on 127.0.0.1:8901 that DesktopTransport (EmbeddedLynx) connects to.
setsid "$BINARY" --url "$LYNX_DEVTOOL_MCP_TESTING_PAGE_URL" \
< /dev/null > /tmp/embedded-lynx.log 2>&1 &
echo "EMBEDDED_LYNX_PID=$!" >> "$GITHUB_ENV"

- name: Wait for debug-router port
run: |
set -euo pipefail
for i in $(seq 1 30); do
if (exec 3<>/dev/tcp/127.0.0.1/8901) 2>/dev/null; then
exec 3>&- 3<&-
echo "EmbeddedLynx debug-router is up on port 8901"
exit 0
fi
sleep 1
done
echo "EmbeddedLynx did not open port 8901 in time" >&2
cat /tmp/embedded-lynx.log >&2 || true
exit 1

- name: E2E (devtool-connector)
working-directory: packages/mcp-servers/devtool-connector
run: node --test --test-concurrency=1 'e2e/**/*.test.ts'

- name: E2E (devtool-mcp-server)
working-directory: packages/mcp-servers/devtool-mcp-server
run: node --test --test-concurrency=1 'e2e/**/*.test.ts'

- name: Stop EmbeddedLynx runtime
if: always()
run: kill "${EMBEDDED_LYNX_PID}" 2>/dev/null || true

- name: Upload EmbeddedLynx log
if: failure()
uses: actions/upload-artifact@v4
with:
name: embedded-lynx-log
path: /tmp/embedded-lynx.log
if-no-files-found: ignore
152 changes: 152 additions & 0 deletions packages/mcp-servers/devtool-connector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# @lynx-js/devtool-connector

`@lynx-js/devtool-connector` provides low-level connectivity for Lynx DevTool. It unifies transport layers for Android / iOS / Desktop, and exposes stable request APIs (such as `CDP`, `App`, and `ListSession`).

It can be used as a TypeScript library in higher-level services like `devtool-mcp-server`.

## Requirements

- Node.js >= 18.19
- At least one available device channel:
- Android: ADB (default `127.0.0.1:5037`)
- iOS: usbmuxd
- Desktop: local `127.0.0.1` port

## Installation

```bash
pnpm add @lynx-js/devtool-connector
```

## Library usage

### 1) Initialize Connector

```ts
import { Connector } from "@lynx-js/devtool-connector";
import { DaemonTransport } from "@lynx-js/devtool-connector/transport";

const transports = [
new DaemonTransport(),
];

const connector = new Connector(transports);
```

Use `DaemonTransport` as the default transport. It automatically manages a local daemon process and reuses stable device connections, so it should be the primary path for client discovery and follow-up requests.

Add other transports only when you need fallback behavior for environments where the daemon is unavailable or cannot discover a target client. When fallback transports are present, `Connector` tries daemon-backed clients first, then the remaining direct platform transports.

Fallback example:

```ts
import { Connector } from "@lynx-js/devtool-connector";
import {
AndroidTransport,
DaemonTransport,
DesktopTransport,
iOSTransport,
} from "@lynx-js/devtool-connector/transport";

const connector = new Connector([
new DaemonTransport(),
new AndroidTransport(),
new iOSTransport(),
new DesktopTransport(),
]);
```

### 2) List devices, clients, and sessions

```ts
const devices = await connector.listDevices();
const clients = await connector.listClients();

if (clients.length === 0) {
throw new Error("No available clients found");
}

const clientId = clients[0].id;
const sessions = await connector.sendListSessionMessage(clientId);
```

### 3) Send CDP / App requests

```ts
const sessionId = sessions[0]?.session_id;
if (!sessionId) {
throw new Error("No session found");
}

const dom = await connector.sendCDPMessage(
clientId,
sessionId,
"DOM.getDocument",
{ depth: -1 },
);

const mainThreadEval = await connector.sendCDPMessage(
clientId,
sessionId,
"Runtime.evaluate",
{ expression: "2 + 2" },
// isMainThread
true,
);

await connector.sendAppMessage(clientId, "App.openPage", {
url: "https://lynxjs.org",
});
```

Pass `true` as the optional `isMainThread` argument to target the main-thread VM. Main-thread CDP requests currently support only `Debugger.*`, `Runtime.*`, `HeapProfiler.*`, and `Profiler.*` methods.

### 4) Streaming APIs (advanced)

The connector also supports streaming send/receive:

- `sendCDPStream(...)`
- `sendStream(...)` (custom pipeline)

These APIs are useful for subscription-style logs, continuous requests, or protocol debugging.

When you finish consuming the returned stream, make sure to close it (dispose the output stream) to release the underlying connection.

Example: consume CDP events with `for await...of`

```ts
import { ReadableStream } from "node:stream/web";

await using outputStream = await connector.sendCDPStream(
clientId,
sessionId,
ReadableStream.from([
{ method: "Runtime.enable" },
]),
);

for await (const message of outputStream) {
// `sendCDPStream` yields CDP events, e.g. { method: "Runtime.consoleAPICalled", params: ... }
console.log(message.method, message.params);
break;
}
```

## Exported entry points

- `@lynx-js/devtool-connector`: `Connector`, `ClientId`, and protocol transform streams
- `@lynx-js/devtool-connector/transport`: platform transport implementations and type definitions
- `@lynx-js/devtool-connector/test-with-client`: helper for integration tests with real clients

## Debugging

Use the `debug` namespace to inspect connection/protocol send-receive details while running code that imports this package:

```bash
DEBUG=devtool-mcp-server:connector* node ./your-script.mjs
```

## Known limitations

- USB-based iOS transport (`iOSTransport`) still does not implement `listAvailableApps` / `openApp`.
- `listClients()` has snapshot semantics: each call re-scans ports and re-validates clients via handshake.
Loading
Loading