Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
67d24b9
feat(mcp): simplify MCP tool architecture and unify platform tools
quanru Nov 25, 2025
77d2bd1
refactor(shared): remove zod dependency and clean up README files acr…
quanru Nov 27, 2025
f3a6a8b
Update packages/shared/src/mcp/base-tools.ts
quanru Nov 27, 2025
e0ae1ca
Update packages/mcp/src/web-tools.ts
quanru Nov 27, 2025
9cfd87e
fix(android): update launchUri to launch in AndroidMidsceneTools
quanru Nov 27, 2025
3299734
feat(android, ios): implement AI-driven app loading and update connec…
quanru Nov 27, 2025
b843d59
fix(mcp): update type definitions path in package.json and enhance rs…
quanru Nov 27, 2025
bcf7a1a
feat(mcp): add default action spaces for Android, iOS, and Web tools;…
quanru Nov 27, 2025
c94a329
feat(mcp): implement temporary device creation for Android, iOS, and …
quanru Nov 28, 2025
8f47ac4
refactor(mcp): remove default action space methods from Android, iOS,…
quanru Nov 28, 2025
1c579af
feat(mcp): add HTTP launch support for Android, iOS, and Web MCP serv…
quanru Nov 28, 2025
f900bc6
refactor(mcp): streamline command line argument parsing and launch lo…
quanru Nov 28, 2025
a7fbb7d
refactor(mcp): enhance type definitions and improve error handling in…
quanru Nov 28, 2025
5563421
refactor(mcp): delegate web MCP server launch to the main MCP package…
quanru Nov 28, 2025
e18c94b
feat(mcp): refactor MCP server structure and tools
quanru Nov 28, 2025
d56be2d
refactor(mcp): improve bridge mode initialization and test organization
quanru Nov 29, 2025
25fb2ee
fix(mcp): address GitHub Copilot review comments from PR #1507
quanru Nov 30, 2025
1637a49
refactor(mcp): improve code quality following yuyutaotao style guide
quanru Dec 1, 2025
d380d75
refactor(mcp): centralize app loading timeout constants in shared pac…
quanru Dec 1, 2025
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,5 @@ AGENTS.md
.github/instructions/nx.instructions.md
.gemini-clipboard
tsconfig.build.tsbuildinfo
.webx
.webx
.mcp.json
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "nx run-many --target=build:watch --exclude=android-playground,chrome-extension,@midscene/report,doc --verbose --parallel=6",
"build": "nx run-many --target=build --exclude=doc --verbose",
"build:skip-cache": "nx run-many --target=build --exclude=doc --verbose --skip-nx-cache",
"test": "nx run-many --target=test --projects=@midscene/core,@midscene/shared,@midscene/visualizer,@midscene/web,@midscene/cli,@midscene/android,@midscene/ios,@midscene/mcp,@midscene/playground --verbose",
"test": "nx run-many --target=test --projects=@midscene/core,@midscene/shared,@midscene/visualizer,@midscene/web,@midscene/cli,@midscene/android,@midscene/ios,@midscene/mcp,@midscene/android-mcp,@midscene/ios-mcp,@midscene/web-mcp,@midscene/playground --verbose",
"test:ai": "nx run-many --target=test:ai --projects=@midscene/core,@midscene/web,@midscene/cli --verbose",
"e2e": "nx run @midscene/web:e2e --verbose --exclude-task-dependencies",
"e2e:cache": "nx run @midscene/web:e2e:cache --verbose --exclude-task-dependencies",
Expand Down Expand Up @@ -56,7 +56,7 @@
"husky": "9.1.7",
"minimist": "1.2.5",
"nano-staged": "^0.8.0",
"nx": "21.1.2",
"nx": "22.1.3",
"prettier": "^3.6.2",
"pretty-quick": "3.1.3",
"semver": "7.5.2",
Expand Down
3 changes: 3 additions & 0 deletions packages/android-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Midscene MCP

docs: https://midscenejs.com/mcp.html
47 changes: 47 additions & 0 deletions packages/android-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@midscene/android-mcp",
"version": "1.0.0",
"description": "Midscene MCP Server for Android automation",
"bin": "dist/index.js",
"files": ["dist"],
"main": "./dist/server.js",
"types": "./dist/server.d.ts",
"exports": {
".": {
"types": "./dist/server.d.ts",
"default": "./dist/server.js"
},
"./server": {
"types": "./dist/server.d.ts",
"default": "./dist/server.js"
}
},
"scripts": {
"build": "rslib build",
"dev": "npm run build:watch",
"build:watch": "rslib build --watch",
"mcp-playground": "npx @modelcontextprotocol/inspector node ./dist/index.js",
"test": "vitest run",
"inspect": "node scripts/inspect.mjs"
},
"devDependencies": {
"@midscene/android": "workspace:*",
"@midscene/core": "workspace:*",
"@midscene/shared": "workspace:*",
"@modelcontextprotocol/inspector": "^0.16.3",
"@modelcontextprotocol/sdk": "1.10.2",
"@rslib/core": "^0.11.2",
"@types/node": "^18.0.0",
"dotenv": "^16.4.5",
"typescript": "^5.8.3",
"vitest": "3.0.5"
},
"dependencies": {
"@silvia-odwyer/photon": "0.3.3",
"@silvia-odwyer/photon-node": "0.3.3",
"bufferutil": "4.0.9",
"sharp": "^0.34.3",
"utf-8-validate": "6.0.5"
},
"license": "MIT"
}
41 changes: 41 additions & 0 deletions packages/android-mcp/rslib.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { defineConfig } from '@rslib/core';
import { version } from './package.json';

export default defineConfig({
source: {
define: {
__VERSION__: `'${version}'`,
},
entry: {
index: './src/index.ts',
server: './src/server.ts',
},
},
output: {
externals: [
(data, cb) => {
if (
data.context?.includes('/node_modules/ws/lib') &&
['bufferutil', 'utf-8-validate'].includes(data.request as string)
) {
cb(undefined, data.request);
}
cb();
},
'@silvia-odwyer/photon',
'@silvia-odwyer/photon-node',
'@modelcontextprotocol/sdk',
],
},
lib: [
{
format: 'cjs',
syntax: 'es2021',
output: {
distPath: {
root: 'dist',
},
},
},
],
});
110 changes: 110 additions & 0 deletions packages/android-mcp/src/android-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { type AndroidAgent, agentFromAdbDevice } from '@midscene/android';
import { z } from '@midscene/core';
import { getDebug } from '@midscene/shared/logger';
import {
type BaseAgent,
BaseMidsceneTools,
type ToolDefinition,
defaultAppLoadingCheckIntervalMs,
defaultAppLoadingTimeoutMs,
} from '@midscene/shared/mcp';

const debug = getDebug('mcp:android-tools');

/**
* Android-specific tools manager
* Extends BaseMidsceneTools to provide Android ADB device connection tools
*/
export class AndroidMidsceneTools extends BaseMidsceneTools {
protected createTemporaryDevice() {
// Use require to avoid circular dependency with @midscene/android
const { AndroidDevice } = require('@midscene/android');
// Create minimal temporary instance without connecting to device
// The constructor doesn't establish ADB connection
return new AndroidDevice('temp-for-action-space', {});
}

protected async ensureAgent(deviceId?: string): Promise<AndroidAgent> {
if (this.agent && deviceId) {
// If a specific deviceId is requested and we have an agent,
// destroy it to create a new one with the new device
try {
await this.agent.destroy?.();
} catch (error) {
debug('Failed to destroy agent during cleanup:', error);
}
this.agent = undefined;
}

if (this.agent) {
return this.agent as unknown as AndroidAgent;
}

debug('Creating Android agent with deviceId:', deviceId || 'auto-detect');
const agent = await agentFromAdbDevice(deviceId);
this.agent = agent as unknown as BaseAgent;
return agent;
}

/**
* Provide Android-specific platform tools
*/
protected preparePlatformTools(): ToolDefinition[] {
return [
{
name: 'android_connect',
description:
'Connect to Android device and optionally launch an app. If deviceId not provided, uses the first available device.',
schema: {
deviceId: z
.string()
.optional()
.describe('Android device ID (from adb devices)'),
uri: z
.string()
.optional()
.describe(
'Optional URI to launch app (e.g., market://details?id=com.example.app)',
),
},
handler: async ({
deviceId,
uri,
}: {
deviceId?: string;
uri?: string;
}) => {
const agent = await this.ensureAgent(deviceId);

// If URI is provided, launch the app
if (uri) {
await agent.page.launch(uri);

// Wait for app to finish loading using AI-driven polling
await agent.aiWaitFor(
'the app has finished loading and is ready to use',
{
timeoutMs: defaultAppLoadingTimeoutMs,
checkIntervalMs: defaultAppLoadingCheckIntervalMs,
},
);
}

const screenshot = await agent.page.screenshotBase64();

return {
content: [
{
type: 'text',
text: `Connected to Android device${deviceId ? `: ${deviceId}` : ' (auto-detected)'}${uri ? ` and launched: ${uri} (app ready)` : ''}`,
},
...this.buildScreenshotContent(screenshot),
],
isError: false,
};
},
autoDestroy: false, // Keep agent alive for subsequent operations
},
];
}
}
12 changes: 12 additions & 0 deletions packages/android-mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env node
import { parseArgs } from 'node:util';
import {
type CLIArgs,
CLI_ARGS_CONFIG,
launchMCPServer,
} from '@midscene/shared/mcp';
import { AndroidMCPServer } from './server.js';

const { values } = parseArgs({ options: CLI_ARGS_CONFIG });

launchMCPServer(new AndroidMCPServer(), values as CLIArgs).catch(console.error);
22 changes: 22 additions & 0 deletions packages/android-mcp/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { BaseMCPServer } from '@midscene/shared/mcp';
import { AndroidMidsceneTools } from './android-tools.js';

declare const __VERSION__: string;

/**
* Android MCP Server
* Provides MCP tools for Android automation through ADB
*/
export class AndroidMCPServer extends BaseMCPServer {
constructor() {
super({
name: '@midscene/android-mcp',
version: __VERSION__,
description: 'Midscene MCP Server for Android automation',
});
}

protected createToolsManager(): AndroidMidsceneTools {
return new AndroidMidsceneTools();
}
}
11 changes: 11 additions & 0 deletions packages/android-mcp/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../shared/tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2021"],
"noEmit": true,
"useDefineForClassFields": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true
},
"include": ["src"]
}
8 changes: 8 additions & 0 deletions packages/android-mcp/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});
6 changes: 6 additions & 0 deletions packages/android/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ export async function agentFromAdbDevice(
if (!deviceId) {
const devices = await getConnectedDevices();

if (devices.length === 0) {
throw new Error(
'No Android devices found. Please connect an Android device and ensure ADB is properly configured. Run `adb devices` to verify device connection.',
);
}

deviceId = devices[0].udid;

debugAgent(
Expand Down
3 changes: 3 additions & 0 deletions packages/ios-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @midscene/ios-mcp

docs: https://midscenejs.com/mcp.html
47 changes: 47 additions & 0 deletions packages/ios-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@midscene/ios-mcp",
"version": "1.0.0",
"description": "Midscene MCP Server for iOS automation",
"bin": "dist/index.js",
"files": ["dist"],
"main": "./dist/server.js",
"types": "./dist/server.d.ts",
"exports": {
".": {
"types": "./dist/server.d.ts",
"default": "./dist/server.js"
},
"./server": {
"types": "./dist/server.d.ts",
"default": "./dist/server.js"
}
},
"scripts": {
"build": "rslib build",
"dev": "npm run build:watch",
"build:watch": "rslib build --watch",
"mcp-playground": "npx @modelcontextprotocol/inspector node ./dist/index.js",
"test": "vitest run",
"inspect": "node scripts/inspect.mjs"
},
"devDependencies": {
"@midscene/ios": "workspace:*",
"@midscene/core": "workspace:*",
"@midscene/shared": "workspace:*",
"@modelcontextprotocol/inspector": "^0.16.3",
"@modelcontextprotocol/sdk": "1.10.2",
"@rslib/core": "^0.11.2",
"@types/node": "^18.0.0",
"dotenv": "^16.4.5",
"typescript": "^5.8.3",
"vitest": "3.0.5"
},
"dependencies": {
"@silvia-odwyer/photon": "0.3.3",
"@silvia-odwyer/photon-node": "0.3.3",
"bufferutil": "4.0.9",
"sharp": "^0.34.3",
"utf-8-validate": "6.0.5"
},
"license": "MIT"
}
41 changes: 41 additions & 0 deletions packages/ios-mcp/rslib.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { defineConfig } from '@rslib/core';
import { version } from './package.json';

export default defineConfig({
source: {
define: {
__VERSION__: `'${version}'`,
},
entry: {
index: './src/index.ts',
server: './src/server.ts',
},
},
output: {
externals: [
(data, cb) => {
if (
data.context?.includes('/node_modules/ws/lib') &&
['bufferutil', 'utf-8-validate'].includes(data.request as string)
) {
cb(undefined, data.request);
}
cb();
},
'@silvia-odwyer/photon',
'@silvia-odwyer/photon-node',
'@modelcontextprotocol/sdk',
],
},
lib: [
{
format: 'cjs',
syntax: 'es2021',
output: {
distPath: {
root: 'dist',
},
},
},
],
});
Loading
Loading