-
Notifications
You must be signed in to change notification settings - Fork 122
refactor(devtool-mcp-server)!: use new connector #2284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
colinaaa
merged 8 commits into
lynx-family:main
from
colinaaa:colin/0302/devtool-connector
Mar 3, 2026
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
8ca86c5
refactor(devtool-mcp-server)!: use new connector
colinaaa 806202a
pnpm dedupe
colinaaa d9ce6d1
docs: fix incorrect paths in devtool-connector AGENTS.md
colinaaa 49ce77c
docs: fix grammar in changeset for devtool-mcp-server
colinaaa 71a04f7
Remove unnecessary ReadableStream.from() wrapping.
colinaaa 9d84257
fix(devtool-connector): fix race condition in connect abort handling
colinaaa 7aa8541
fix: resolve sherif dependency version conflicts
colinaaa 31c24d9
Merge branch 'main' into colin/0302/devtool-connector
PupilTong File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "@lynx-js/devtool-mcp-server": minor | ||
| --- | ||
|
|
||
| Use `@lynx-js/devtool-connector` instead of `@lynx-js/debug-router-connector`. | ||
|
|
||
| The new connector avoids using keep-alive connections, which makes the connection much more reliable. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@lynx-js/devtool-mcp-server": minor | ||
| --- | ||
|
|
||
| **BREAKING CHANGE**: Remove the `./debug-router-connector` exports. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Copyright 2025 The Lynx Authors. All rights reserved. | ||
| // Licensed under the Apache License Version 2.0 that can be found in the | ||
| // LICENSE file in the root directory of this source tree. | ||
|
|
||
| import eslint from '@eslint/js'; | ||
| import { defineConfig, globalIgnores } from 'eslint/config'; | ||
| import headers from 'eslint-plugin-headers'; | ||
| import tseslint from 'typescript-eslint'; | ||
|
|
||
| export default defineConfig( | ||
| globalIgnores([ | ||
| '.rslib/**', | ||
| 'dist/**', | ||
| 'node_modules/**', | ||
| ]), | ||
| eslint.configs.recommended, | ||
| tseslint.configs.recommended, | ||
| { | ||
| languageOptions: { | ||
| parserOptions: { | ||
| tsconfigRootDir: import.meta.dirname, | ||
| projectService: { | ||
| allowDefaultProject: ['*.js', 'rslib.config.ts'], | ||
| defaultProject: './tsconfig.json', | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| plugins: { | ||
| headers, | ||
| }, | ||
| rules: { | ||
| 'headers/header-format': [ | ||
| 'error', | ||
| { | ||
| source: 'string', | ||
| style: 'line', | ||
| content: [ | ||
| 'Copyright (year) {authors}. All rights reserved.', | ||
| 'Licensed under the (license) that can be found in the', | ||
| 'LICENSE file in the root directory of this source tree.', | ||
| ].join('\n'), | ||
| variables: { | ||
| authors: 'The Lynx Authors', | ||
| }, | ||
| patterns: { | ||
| year: { | ||
| pattern: '\\d{4}', | ||
| defaultValue: new Date().getFullYear().toString(), | ||
| }, | ||
| license: { | ||
| pattern: [ | ||
| 'Apache License Version 2.0', | ||
| ].join('|'), | ||
| defaultValue: 'Apache License Version 2.0', | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| { | ||
| "name": "@lynx-js/devtool-connector", | ||
| "version": "0.1.0", | ||
| "type": "module", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./src/index.ts", | ||
| "import": "./src/index.ts", | ||
| "default": "./src/index.ts" | ||
| }, | ||
| "./transport": { | ||
| "types": "./src/transport/index.ts", | ||
| "import": "./src/transport/index.ts", | ||
| "default": "./src/transport/index.ts" | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "main": "./src/index.ts", | ||
| "types": "./src/index.ts", | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "scripts": { | ||
| "build": "rslib build", | ||
| "test": "node --test" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/debug": "^4.1.12", | ||
| "@types/node": "^24.10.13", | ||
| "@yume-chan/adb": "catalog:adb", | ||
| "@yume-chan/adb-server-node-tcp": "catalog:adb", | ||
| "@yume-chan/stream-extra": "catalog:adb", | ||
| "commander": "^13.1.0", | ||
| "debug": "^4.4.3", | ||
| "rsbuild-plugin-publint": "0.3.4", | ||
| "typescript": "^5.9.3", | ||
| "usbmux-client": "^0.2.1" | ||
| }, | ||
|
colinaaa marked this conversation as resolved.
|
||
| "engines": { | ||
| "node": ">=18.19" | ||
| }, | ||
| "publishConfig": { | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "import": "./dist/index.js", | ||
| "default": "./dist/index.js" | ||
| }, | ||
| "./transport": { | ||
| "types": "./dist/transport/index.d.ts", | ||
| "import": "./dist/transport/index.js", | ||
| "default": "./dist/transport/index.js" | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "main": "./dist/index.js", | ||
| "types": "./dist/index.d.ts" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // Copyright 2026 The Lynx Authors. All rights reserved. | ||
| // Licensed under the Apache License Version 2.0 that can be found in the | ||
| // LICENSE file in the root directory of this source tree. | ||
| import { defineConfig } from '@rslib/core'; | ||
| import { pluginPublint } from 'rsbuild-plugin-publint'; | ||
|
|
||
| export default defineConfig({ | ||
| plugins: [ | ||
| pluginPublint({ throwOn: 'suggestion' }), | ||
| ], | ||
| source: { | ||
| entry: { | ||
| index: './src/index.ts', | ||
| 'transport/index': './src/transport/index.ts', | ||
| }, | ||
|
colinaaa marked this conversation as resolved.
|
||
| }, | ||
| lib: [ | ||
| { | ||
| format: 'esm', | ||
| syntax: 'es2022', | ||
| dts: { | ||
| bundle: false, | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # Connector Design Document | ||
|
|
||
| ## 1. Context & Goals | ||
|
|
||
| The new Connector is designed to address stability issues in the previous long-lived connection solution. The core idea is to adopt a stateless short-lived connection design, bypass higher-level wrappers, and directly control the lifecycle of the underlying connections. | ||
|
|
||
| ### Key Objectives | ||
|
|
||
| - **Stability first**: Use short-lived connections to avoid the fragility of long-lived ones. | ||
| - **Stateless design**: Each request (for example a CDP request) runs its own `connect -> send -> receive -> close` flow. | ||
| - **Cross-platform**: Provide a unified abstraction for Android (ADB) and iOS (Usbmuxd). | ||
|
|
||
| ## 2. Architecture Layering | ||
|
|
||
| The Connector adopts a layered architecture to separate concerns: | ||
|
|
||
| ### Transport layer | ||
|
|
||
| - **Responsibility**: Establish raw byte-stream channels, without awareness of higher-level protocols. | ||
| - **Implementation**: | ||
| - `AndroidTransport` (Android): Based on `@yume-chan/adb`, uses `adb shell nc` to create a tunnel directly to the device port. | ||
| - `iOSTransport` (iOS): Based on `usbmux-client`, uses `createDeviceTunnel` to connect directly to the device port. | ||
| - **Interface**: Provides primitive operations such as `connect` and `listDevices`. | ||
|
|
||
| ### Protocol layer | ||
|
|
||
| - **Responsibility**: Handle Peertalk framing and CDP message encoding/decoding. | ||
| - **Components**: | ||
| - `MessageToPeertalkTransformStream`: Message -> Peertalk frame. | ||
| - `PeertalkToMessageTransformStream`: Peertalk frame -> Message. | ||
| - **Characteristics**: Pure functional transforms, stateless. | ||
|
|
||
| ### Connector layer (core) | ||
|
|
||
| - **Responsibility**: Expose a unified interface and coordinate the Transport and Protocol layers. | ||
| - **Implementation**: `Connector` class in `src/index.ts`. | ||
| - **Mechanics**: | ||
| - **Short-lived requests**: Each call to `sendMessageWithTransport` establishes a new connection, transfers data, and then closes the connection immediately. | ||
| - **Device/client discovery**: | ||
| - Android: Scan ports 8901-8910 and send an `Initialize` handshake message to confirm the presence of a Client. | ||
| - All results are snapshots of the current moment. | ||
|
|
||
| ## 3. Core Concepts | ||
|
|
||
| In the stateless model, the lifecycle of core objects changes: | ||
|
|
||
| - **Device**: A physical device, discovered dynamically via low-level tools (ADB/Usbmuxd). | ||
| - **Client**: A listening port of `debug-router` on the device (for example 8901-8910). | ||
| - **Dynamic nature**: We no longer hold client objects for a long time; instead, we scan ports and use Initialize/Register handshakes to confirm their existence. | ||
| - **Identifier**: `ClientId` contains the device ID and port number. | ||
| - **Session**: A page/view session. Only fetched via `ListSession` when needed and treated as a temporary snapshot. | ||
|
|
||
| ## 4. Code Structure | ||
|
|
||
| - `src/index.ts`: Connector layer. Main entry, manages all transports. | ||
| - `src/transport/transport.ts`: Transport interface definition. | ||
| - `src/transport/android.ts`: `AndroidTransport` implementation. | ||
| - `src/transport/ios.ts`: `iOSTransport` implementation. | ||
| - `src/streams/peertalk.ts` / `src/streams/cdp.ts`: Protocol layer stream transformers. | ||
| - `src/types.ts`: Core type definitions. | ||
|
|
||
| ## 5. Development Guide | ||
|
|
||
| - **Adding a new transport**: Implement the `Transport` interface and register it in the `Connector`. | ||
| - **Debugging**: Use `DEBUG=devtool-mcp-server:connector*` to enable detailed logs. | ||
| - **Notes**: Any new APIs should prefer short-lived connection implementations and avoid introducing persistent state dependencies. | ||
|
|
||
| ## 6. API & Usage Examples | ||
|
|
||
| The Connector is the single entry point for upper-layer tools to communicate with devices. | ||
|
|
||
| ### 6.1 Initialization | ||
|
|
||
| ```typescript | ||
| import { AndroidTransport } from './transport/android.js'; | ||
| import { Connector } from './index.js'; | ||
| import { iOSTransport } from './transport/ios.js'; | ||
|
|
||
| // Initialize the Connector with support for both Android and iOS | ||
| const connector = new Connector([ | ||
| new AndroidTransport(), | ||
| new iOSTransport(), | ||
| ]); | ||
| ``` | ||
|
|
||
| ### 6.2 Device discovery and connection | ||
|
|
||
| ```typescript | ||
| // 1. Get all connected devices (Android + iOS) | ||
| const devices = await connector.listDevices(); | ||
| // Output: [{ id: "emulator-5554", os: "Android" }, { id: "00008101-...", os: "iOS" }] | ||
|
|
||
| // 2. Get all active debug clients on a device (debug-router ports) | ||
| // Note: this automatically scans device ports and performs a handshake check | ||
| const clients = await connector.listClients(); | ||
| // Output: [{ id: "emulator-5554:8901", info: { ... } }, ...] | ||
| ``` | ||
|
|
||
| ### 6.3 Sending CDP commands | ||
|
|
||
| This is the most common operation. Because connections are short-lived, each call is atomic. | ||
|
|
||
| ```typescript | ||
| const clientId = 'emulator-5554:8901'; | ||
|
|
||
| // 1. Get the list of sessions (pages) under a client | ||
| const sessions = await connector.sendListSessionMessage(clientId); | ||
| if (sessions.length > 0) { | ||
| const sessionId = sessions[0].session_id; | ||
|
|
||
| // 2. Send a CDP command (for example, fetch the DOM) | ||
| const result = await connector.sendCDPMessage( | ||
| clientId, | ||
| sessionId, | ||
| 'DOM.getDocument', | ||
| { depth: -1 }, | ||
| ); | ||
| console.log(result); | ||
| } | ||
| ``` | ||
|
|
||
| ### 6.4 App control | ||
|
|
||
| ```typescript | ||
| const deviceId = 'emulator-5554'; | ||
|
|
||
| // List installed debuggable apps | ||
| const apps = await connector.listAvailableApps(deviceId); | ||
|
|
||
| // Open an app | ||
| await connector.openApp(deviceId, 'com.lynx.explorer'); | ||
| ``` |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.