Skip to content

Commit 6f4df9d

Browse files
authored
feat: add devtool-connector and devtool-mcp-server packages (#86)
* feat: add devtool-connector and devtool-mcp-server packages Migrate devtool packages from lynx-stack to this repository, updated to match the current internal version level: - @lynx-js/devtool-connector v0.9.3 (was v0.1.1 in lynx-stack) - @lynx-js/devtool-mcp-server v0.13.3 (was v0.5.1 in lynx-stack) - @lynx-js/skill-lynx-devtool updated to v0.13.3 New packages: - mcp-servers/devtool-connector: Transport layer for communicating with Lynx devices (Android/iOS/Desktop + background daemon) - mcp-servers/devtool-mcp-server: MCP server with 46 tools across 13 domains (App, CSS, DOM, Debugger, Device, HeapProfiler, Input, Lynx, Memory, Page, Performance, Runtime, UITree) Updated skill: - 16 commands (added global-switch, inspect, take-heap-snapshot, recorder-start/end/analysis, reactlynx) - Daemon transport support for persistent device connections CI: - Added E2E test job using EmbeddedLynx binary on Linux * ci: add dedicated lynx-devtool e2e workflow with EmbeddedLynx - Add .github/workflows/lynx-devtool.yml that downloads and launches the EmbeddedLynx headless runtime, waits for its debug-router port, then runs the connector and mcp-server e2e suites against it. - Remove the inline e2e job from test.yml (the binary was never launched there, so the tests could not find a client/session). - Add adapted devtool-mcp-server e2e (tools.test.ts) covering the open-source tool set against EmbeddedLynx. - Remove internal-only webview-cdp e2e (Douyin WebView parity test depending on internal CDP reference docs). - Skip background-thread heap snapshot on EmbeddedLynx (unsupported). * refactor: move mcp-servers under packages/ Relocate devtool-connector and devtool-mcp-server from top-level mcp-servers/ to packages/mcp-servers/ for consistency with the rest of the workspace layout. Update pnpm-workspace.yaml glob, the lynx-devtool e2e workflow paths, and the connector tsconfig extends path accordingly. * test(lynx-devtool): enable reactlynx e2e against public bundle - Add reactlynx e2e to the skill package, driving the full init -> refresh -> operation_v2 -> inspect -> update-prop pipeline against a real ReactLynx page. - Use the public @lynx-example/react-devtool bundle (ships @lynx-js/preact-devtools) so the test runs on EmbeddedLynx; the former EmbeddedLynx skip is removed. - Match 'Provider' (present in the minified bundle) instead of the internal-specific 'view' component name. - Extend the e2e workflow to launch EmbeddedLynx twice: the Swiper bundle for connector/mcp-server tools, then the ReactLynx bundle for the reactlynx suite. Factor the port-readiness wait into a shared .github/scripts/wait-debug-router.sh. * style: apply biome/eslint formatting and add changeset - Format all migrated devtool files to the skills repo style (single quotes, no trailing commas) via biome. - Remove eslint-disable references to the eslint-plugin-n rules (n/no-unsupported-features/node-builtins, n/no-process-exit) that are not configured in this repo; fix no-explicit-any / no-unused-vars in migrated test utilities. - Add a changeset for the three devtool packages.
1 parent 491a973 commit 6f4df9d

173 files changed

Lines changed: 20091 additions & 361 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@lynx-js/devtool-connector": minor
3+
"@lynx-js/devtool-mcp-server": minor
4+
"@lynx-js/skill-lynx-devtool": minor
5+
---
6+
7+
Add the Lynx DevTool packages: `@lynx-js/devtool-connector` (device transport layer with background daemon), `@lynx-js/devtool-mcp-server` (MCP server exposing DOM/CSS/Runtime/Performance and more tools), and an updated `@lynx-js/skill-lynx-devtool` skill with screenshot, console, heap snapshot, recorder, and ReactLynx inspection commands.

.changeset/config.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
33
"changelog": "@changesets/cli/changelog",
44
"commit": false,
5-
"fixed": [],
5+
"fixed": [
6+
[
7+
"@lynx-js/devtool-connector",
8+
"@lynx-js/devtool-mcp-server",
9+
"@lynx-js/skill-lynx-devtool"
10+
]
11+
],
612
"linked": [],
713
"access": "public",
814
"baseBranch": "main",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
# Wait until the EmbeddedLynx debug-router is listening on 127.0.0.1:8901.
3+
# Usage: wait-debug-router.sh <log-file>
4+
set -euo pipefail
5+
6+
LOG_FILE="${1:-}"
7+
8+
for _ in $(seq 1 30); do
9+
if (exec 3<>/dev/tcp/127.0.0.1/8901) 2>/dev/null; then
10+
exec 3>&- 3<&-
11+
echo "EmbeddedLynx debug-router is up on port 8901"
12+
exit 0
13+
fi
14+
sleep 1
15+
done
16+
17+
echo "EmbeddedLynx did not open port 8901 in time" >&2
18+
if [ -n "$LOG_FILE" ] && [ -f "$LOG_FILE" ]; then
19+
tail -n 50 "$LOG_FILE" >&2 || true
20+
fi
21+
exit 1

.github/workflows/lynx-devtool.yml

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
name: Lynx DevTool E2E
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
paths:
7+
- "packages/mcp-servers/devtool-connector/**"
8+
- "packages/mcp-servers/devtool-mcp-server/**"
9+
- "packages/skills/lynx-devtool/**"
10+
- ".github/workflows/lynx-devtool.yml"
11+
push:
12+
branches: [main]
13+
paths:
14+
- "packages/mcp-servers/devtool-connector/**"
15+
- "packages/mcp-servers/devtool-mcp-server/**"
16+
- "packages/skills/lynx-devtool/**"
17+
- ".github/workflows/lynx-devtool.yml"
18+
workflow_dispatch:
19+
20+
env:
21+
EMBEDDED_LYNX_TARBALL: https://github.com/lynx-community/skills/releases/download/embedded-lynx-202606041609/embedded-lynx-linux-x86_64.tar.gz
22+
EMBEDDED_LYNX_BINARY: /tmp/embedded-lynx-linux-x86_64/embedded_lynx
23+
LYNX_DEVTOOL_MCP_TESTING_TRANSPORTS: EmbeddedLynx
24+
LYNX_DEVTOOL_MCP_TESTING_APP_PACKAGE: EmbeddedLynx
25+
# Generic Lynx page for connector / mcp-server tools.
26+
SWIPER_BUNDLE_URL: https://lynxjs.org/lynx-examples/swiper/dist/Swiper.lynx.bundle
27+
# ReactLynx page bundled with @lynx-js/preact-devtools for reactlynx tooling.
28+
REACT_DEVTOOL_BUNDLE_URL: https://unpkg.com/@lynx-example/react-devtool@0.2.0/dist/main.lynx.bundle
29+
30+
jobs:
31+
e2e:
32+
runs-on: ubuntu-latest
33+
timeout-minutes: 25
34+
steps:
35+
- name: Checkout
36+
uses: actions/checkout@v5
37+
38+
- name: Install Pnpm
39+
run: npm i -g corepack@latest --force && corepack enable
40+
41+
- name: Setup Node.js
42+
uses: actions/setup-node@v4
43+
with:
44+
node-version: 24
45+
cache: "pnpm"
46+
47+
- name: Install Dependencies
48+
run: pnpm install
49+
50+
- name: Download EmbeddedLynx
51+
run: |
52+
set -euo pipefail
53+
curl -sL "$EMBEDDED_LYNX_TARBALL" | tar -xzf - -C /tmp
54+
chmod +x "$EMBEDDED_LYNX_BINARY"
55+
56+
# --- Generic tools: connector + mcp-server against the Swiper bundle ---
57+
58+
- name: Start EmbeddedLynx (Swiper bundle)
59+
run: |
60+
set -euo pipefail
61+
setsid "$EMBEDDED_LYNX_BINARY" --url "$SWIPER_BUNDLE_URL" \
62+
< /dev/null > /tmp/embedded-lynx-swiper.log 2>&1 &
63+
echo "EMBEDDED_LYNX_PID=$!" >> "$GITHUB_ENV"
64+
./.github/scripts/wait-debug-router.sh /tmp/embedded-lynx-swiper.log
65+
66+
- name: E2E (devtool-connector)
67+
working-directory: packages/mcp-servers/devtool-connector
68+
env:
69+
LYNX_DEVTOOL_MCP_TESTING_PAGE_URL: ${{ env.SWIPER_BUNDLE_URL }}
70+
LYNX_DEVTOOL_MCP_TESTING_OPEN_URL: ${{ env.SWIPER_BUNDLE_URL }}
71+
run: node --test --test-concurrency=1 'e2e/**/*.test.ts'
72+
73+
- name: E2E (devtool-mcp-server)
74+
working-directory: packages/mcp-servers/devtool-mcp-server
75+
env:
76+
LYNX_DEVTOOL_MCP_TESTING_PAGE_URL: ${{ env.SWIPER_BUNDLE_URL }}
77+
LYNX_DEVTOOL_MCP_TESTING_OPEN_URL: ${{ env.SWIPER_BUNDLE_URL }}
78+
run: node --test --test-concurrency=1 'e2e/**/*.test.ts'
79+
80+
- name: Stop EmbeddedLynx (Swiper bundle)
81+
if: always()
82+
run: kill "${EMBEDDED_LYNX_PID}" 2>/dev/null || true
83+
84+
# --- ReactLynx: skill against the preact-devtools-enabled bundle ---
85+
86+
- name: Start EmbeddedLynx (ReactLynx bundle)
87+
run: |
88+
set -euo pipefail
89+
setsid "$EMBEDDED_LYNX_BINARY" --url "$REACT_DEVTOOL_BUNDLE_URL" \
90+
< /dev/null > /tmp/embedded-lynx-reactlynx.log 2>&1 &
91+
echo "EMBEDDED_LYNX_REACT_PID=$!" >> "$GITHUB_ENV"
92+
./.github/scripts/wait-debug-router.sh /tmp/embedded-lynx-reactlynx.log
93+
94+
- name: E2E (skill reactlynx)
95+
working-directory: packages/skills/lynx-devtool
96+
env:
97+
LYNX_DEVTOOL_MCP_TESTING_PAGE_URL: ${{ env.REACT_DEVTOOL_BUNDLE_URL }}
98+
LYNX_DEVTOOL_MCP_TESTING_OPEN_URL: ${{ env.REACT_DEVTOOL_BUNDLE_URL }}
99+
run: node --test --test-concurrency=1 'e2e/**/*.test.ts'
100+
101+
- name: Stop EmbeddedLynx (ReactLynx bundle)
102+
if: always()
103+
run: kill "${EMBEDDED_LYNX_REACT_PID}" 2>/dev/null || true
104+
105+
- name: Upload EmbeddedLynx logs
106+
if: failure()
107+
uses: actions/upload-artifact@v4
108+
with:
109+
name: embedded-lynx-logs
110+
path: /tmp/embedded-lynx-*.log
111+
if-no-files-found: ignore
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# @lynx-js/devtool-connector
2+
3+
`@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`).
4+
5+
It can be used as a TypeScript library in higher-level services like `devtool-mcp-server`.
6+
7+
## Requirements
8+
9+
- Node.js >= 18.19
10+
- At least one available device channel:
11+
- Android: ADB (default `127.0.0.1:5037`)
12+
- iOS: usbmuxd
13+
- Desktop: local `127.0.0.1` port
14+
15+
## Installation
16+
17+
```bash
18+
pnpm add @lynx-js/devtool-connector
19+
```
20+
21+
## Library usage
22+
23+
### 1) Initialize Connector
24+
25+
```ts
26+
import { Connector } from "@lynx-js/devtool-connector";
27+
import { DaemonTransport } from "@lynx-js/devtool-connector/transport";
28+
29+
const transports = [
30+
new DaemonTransport(),
31+
];
32+
33+
const connector = new Connector(transports);
34+
```
35+
36+
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.
37+
38+
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.
39+
40+
Fallback example:
41+
42+
```ts
43+
import { Connector } from "@lynx-js/devtool-connector";
44+
import {
45+
AndroidTransport,
46+
DaemonTransport,
47+
DesktopTransport,
48+
iOSTransport,
49+
} from "@lynx-js/devtool-connector/transport";
50+
51+
const connector = new Connector([
52+
new DaemonTransport(),
53+
new AndroidTransport(),
54+
new iOSTransport(),
55+
new DesktopTransport(),
56+
]);
57+
```
58+
59+
### 2) List devices, clients, and sessions
60+
61+
```ts
62+
const devices = await connector.listDevices();
63+
const clients = await connector.listClients();
64+
65+
if (clients.length === 0) {
66+
throw new Error("No available clients found");
67+
}
68+
69+
const clientId = clients[0].id;
70+
const sessions = await connector.sendListSessionMessage(clientId);
71+
```
72+
73+
### 3) Send CDP / App requests
74+
75+
```ts
76+
const sessionId = sessions[0]?.session_id;
77+
if (!sessionId) {
78+
throw new Error("No session found");
79+
}
80+
81+
const dom = await connector.sendCDPMessage(
82+
clientId,
83+
sessionId,
84+
"DOM.getDocument",
85+
{ depth: -1 },
86+
);
87+
88+
const mainThreadEval = await connector.sendCDPMessage(
89+
clientId,
90+
sessionId,
91+
"Runtime.evaluate",
92+
{ expression: "2 + 2" },
93+
// isMainThread
94+
true,
95+
);
96+
97+
await connector.sendAppMessage(clientId, "App.openPage", {
98+
url: "https://lynxjs.org",
99+
});
100+
```
101+
102+
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.
103+
104+
### 4) Streaming APIs (advanced)
105+
106+
The connector also supports streaming send/receive:
107+
108+
- `sendCDPStream(...)`
109+
- `sendStream(...)` (custom pipeline)
110+
111+
These APIs are useful for subscription-style logs, continuous requests, or protocol debugging.
112+
113+
When you finish consuming the returned stream, make sure to close it (dispose the output stream) to release the underlying connection.
114+
115+
Example: consume CDP events with `for await...of`
116+
117+
```ts
118+
import { ReadableStream } from "node:stream/web";
119+
120+
await using outputStream = await connector.sendCDPStream(
121+
clientId,
122+
sessionId,
123+
ReadableStream.from([
124+
{ method: "Runtime.enable" },
125+
]),
126+
);
127+
128+
for await (const message of outputStream) {
129+
// `sendCDPStream` yields CDP events, e.g. { method: "Runtime.consoleAPICalled", params: ... }
130+
console.log(message.method, message.params);
131+
break;
132+
}
133+
```
134+
135+
## Exported entry points
136+
137+
- `@lynx-js/devtool-connector`: `Connector`, `ClientId`, and protocol transform streams
138+
- `@lynx-js/devtool-connector/transport`: platform transport implementations and type definitions
139+
- `@lynx-js/devtool-connector/test-with-client`: helper for integration tests with real clients
140+
141+
## Debugging
142+
143+
Use the `debug` namespace to inspect connection/protocol send-receive details while running code that imports this package:
144+
145+
```bash
146+
DEBUG=devtool-mcp-server:connector* node ./your-script.mjs
147+
```
148+
149+
## Known limitations
150+
151+
- USB-based iOS transport (`iOSTransport`) still does not implement `listAvailableApps` / `openApp`.
152+
- `listClients()` has snapshot semantics: each call re-scans ports and re-validates clients via handshake.

0 commit comments

Comments
 (0)