diff --git a/.gitignore b/.gitignore index a4853d402..0fdda3c4a 100644 --- a/.gitignore +++ b/.gitignore @@ -127,4 +127,5 @@ CLAUDE.md .github/instructions/nx.instructions.md .gemini-clipboard tsconfig.build.tsbuildinfo -.webx \ No newline at end of file +.webx +.mcp.json \ No newline at end of file diff --git a/package.json b/package.json index ffc6b45e7..c4a65bdcc 100644 --- a/package.json +++ b/package.json @@ -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/android-mcp,@midscene/ios-mcp,@midscene/web-bridge-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", @@ -56,8 +56,8 @@ "husky": "9.1.7", "minimist": "1.2.5", "nano-staged": "^0.8.0", - "nx": "21.1.2", - "prettier": "^3.7.4", + "nx": "22.1.3", + "prettier": "^3.6.2", "pretty-quick": "3.1.3", "semver": "7.5.2", "simple-git-hooks": "^2.13.1" diff --git a/packages/android-mcp/README.md b/packages/android-mcp/README.md new file mode 100644 index 000000000..64c48312a --- /dev/null +++ b/packages/android-mcp/README.md @@ -0,0 +1,3 @@ +# Midscene MCP + +docs: https://midscenejs.com/mcp.html diff --git a/packages/android-mcp/package.json b/packages/android-mcp/package.json new file mode 100644 index 000000000..1ade4ffce --- /dev/null +++ b/packages/android-mcp/package.json @@ -0,0 +1,48 @@ +{ + "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.18.3", + "@rspack/core": "1.6.6", + "@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" +} diff --git a/packages/android-mcp/rslib.config.ts b/packages/android-mcp/rslib.config.ts new file mode 100644 index 000000000..4c7736b56 --- /dev/null +++ b/packages/android-mcp/rslib.config.ts @@ -0,0 +1,56 @@ +import { defineConfig } from '@rslib/core'; +import { rspack } from '@rspack/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', + ], + }, + tools: { + rspack: { + plugins: [ + new rspack.BannerPlugin({ + banner: '#!/usr/bin/env node', + raw: true, + test: /^index\.js$/, + }), + ], + optimization: { + minimize: false, + }, + }, + }, + lib: [ + { + format: 'cjs', + syntax: 'es2021', + output: { + distPath: { + root: 'dist', + }, + }, + }, + ], +}); diff --git a/packages/android-mcp/src/android-tools.ts b/packages/android-mcp/src/android-tools.ts new file mode 100644 index 000000000..2dd8d27b8 --- /dev/null +++ b/packages/android-mcp/src/android-tools.ts @@ -0,0 +1,112 @@ +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 { + 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, { + autoDismissKeyboard: false, + }); + 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 + }, + ]; + } +} diff --git a/packages/android-mcp/src/index.ts b/packages/android-mcp/src/index.ts new file mode 100644 index 000000000..a69eb6de8 --- /dev/null +++ b/packages/android-mcp/src/index.ts @@ -0,0 +1,19 @@ +import { parseArgs } from 'node:util'; +import { type CLIArgs, CLI_ARGS_CONFIG } from '@midscene/shared/mcp'; +import { AndroidMCPServer } from './server.js'; + +const { values } = parseArgs({ options: CLI_ARGS_CONFIG }); +const args = values as CLIArgs; + +const server = new AndroidMCPServer(); + +if (args.mode === 'http') { + server + .launchHttp({ + port: Number.parseInt(args.port || '3000', 10), + host: args.host || 'localhost', + }) + .catch(console.error); +} else { + server.launch().catch(console.error); +} diff --git a/packages/android-mcp/src/server.ts b/packages/android-mcp/src/server.ts new file mode 100644 index 000000000..953af7c9a --- /dev/null +++ b/packages/android-mcp/src/server.ts @@ -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(); + } +} diff --git a/packages/android-mcp/tests/http-server.test.ts b/packages/android-mcp/tests/http-server.test.ts new file mode 100644 index 000000000..4c9947469 --- /dev/null +++ b/packages/android-mcp/tests/http-server.test.ts @@ -0,0 +1,99 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { AndroidMCPServer } from '../src/server.js'; + +describe('AndroidMCPServer HTTP mode', () => { + let server: AndroidMCPServer; + const testPort = 13580; // Use different port than web-bridge-mcp + const testHost = '127.0.0.1'; // Use IPv4 explicitly to avoid IPv6 issues in CI + + beforeAll(async () => { + server = new AndroidMCPServer(); + }); + + afterAll(async () => { + // Cleanup will be handled by process exit + }); + + it('should start HTTP server successfully', async () => { + // Mock process.exit to prevent test exit + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit called'); + }) as any); + + try { + // Start server in background and handle potential errors + const serverPromise = server.launchHttp({ + port: testPort, + host: testHost, + }); + + // Catch any errors from server startup without blocking + serverPromise.catch((error) => { + console.error('Server startup error:', error); + }); + + // Wait for server to start with retries (up to 5 seconds) + let connected = false; + for (let i = 0; i < 10; i++) { + await new Promise((resolve) => setTimeout(resolve, 500)); + try { + const response = await fetch(`http://${testHost}:${testPort}/mcp`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'initialize', + params: { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { + name: 'test-client', + version: '1.0.0', + }, + }, + id: 1, + }), + }); + + // Server should respond (even if initialization fails without device) + expect(response.status).toBeGreaterThanOrEqual(200); + expect(response.status).toBeLessThan(600); + connected = true; + console.log( + `✓ Android MCP server started and responding (attempt ${i + 1})`, + ); + break; + } catch (error) { + if (i === 9) { + throw error; // Throw on last attempt + } + // Otherwise continue retrying + } + } + + expect(connected).toBe(true); + } finally { + exitSpy.mockRestore(); + } + }, 15000); // Increase timeout for CI + + it('should reject invalid port numbers', async () => { + const invalidServer = new AndroidMCPServer(); + + await expect( + invalidServer.launchHttp({ + port: -1, + host: testHost, + }), + ).rejects.toThrow(/Invalid port number/); + + await expect( + invalidServer.launchHttp({ + port: 99999, + host: testHost, + }), + ).rejects.toThrow(/Invalid port number/); + }); +}); diff --git a/packages/android-mcp/tsconfig.json b/packages/android-mcp/tsconfig.json new file mode 100644 index 000000000..83e2763d7 --- /dev/null +++ b/packages/android-mcp/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../shared/tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2021"], + "noEmit": true, + "useDefineForClassFields": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/packages/android-mcp/vitest.config.ts b/packages/android-mcp/vitest.config.ts new file mode 100644 index 000000000..7c747ac04 --- /dev/null +++ b/packages/android-mcp/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; +import { version } from './package.json'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + define: { + __VERSION__: JSON.stringify(version), + }, + ssr: { + external: ['@silvia-odwyer/photon'], + }, +}); diff --git a/packages/android-playground/package.json b/packages/android-playground/package.json index 7a868af18..9d9da3b8b 100644 --- a/packages/android-playground/package.json +++ b/packages/android-playground/package.json @@ -33,7 +33,7 @@ "socket.io": "^4.8.1" }, "devDependencies": { - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node": "^18.0.0", diff --git a/packages/android/package.json b/packages/android/package.json index bc4bac27a..57b31c810 100644 --- a/packages/android/package.json +++ b/packages/android/package.json @@ -38,7 +38,7 @@ }, "devDependencies": { "@midscene/playground": "workspace:*", - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", "@types/node": "^18.0.0", "dotenv": "^16.4.5", "typescript": "^5.8.3", diff --git a/packages/android/src/agent.ts b/packages/android/src/agent.ts index da0d4f8d3..f8a3b07fb 100644 --- a/packages/android/src/agent.ts +++ b/packages/android/src/agent.ts @@ -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( diff --git a/packages/cli/package.json b/packages/cli/package.json index fbe0430ae..777e31466 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,7 +29,7 @@ "puppeteer": "24.2.0" }, "devDependencies": { - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", "@types/js-yaml": "4.0.9", "@types/lodash.merge": "4.6.9", "@types/minimist": "1.2.5", diff --git a/packages/core/package.json b/packages/core/package.json index 45d6ef659..2ebbe3ff8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -85,7 +85,7 @@ "socks-proxy-agent": "8.0.4" }, "devDependencies": { - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", "@types/node": "^18.0.0", "@types/node-fetch": "2.6.11", "@types/js-yaml": "4.0.9", diff --git a/packages/ios-mcp/README.md b/packages/ios-mcp/README.md new file mode 100644 index 000000000..d5045919a --- /dev/null +++ b/packages/ios-mcp/README.md @@ -0,0 +1,3 @@ +# @midscene/ios-mcp + +docs: https://midscenejs.com/mcp.html diff --git a/packages/ios-mcp/package.json b/packages/ios-mcp/package.json new file mode 100644 index 000000000..937a0cb0a --- /dev/null +++ b/packages/ios-mcp/package.json @@ -0,0 +1,48 @@ +{ + "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/core": "workspace:*", + "@midscene/ios": "workspace:*", + "@midscene/shared": "workspace:*", + "@modelcontextprotocol/inspector": "^0.16.3", + "@modelcontextprotocol/sdk": "1.10.2", + "@rslib/core": "^0.18.3", + "@rspack/core": "1.6.6", + "@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" +} diff --git a/packages/ios-mcp/rslib.config.ts b/packages/ios-mcp/rslib.config.ts new file mode 100644 index 000000000..4c7736b56 --- /dev/null +++ b/packages/ios-mcp/rslib.config.ts @@ -0,0 +1,56 @@ +import { defineConfig } from '@rslib/core'; +import { rspack } from '@rspack/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', + ], + }, + tools: { + rspack: { + plugins: [ + new rspack.BannerPlugin({ + banner: '#!/usr/bin/env node', + raw: true, + test: /^index\.js$/, + }), + ], + optimization: { + minimize: false, + }, + }, + }, + lib: [ + { + format: 'cjs', + syntax: 'es2021', + output: { + distPath: { + root: 'dist', + }, + }, + }, + ], +}); diff --git a/packages/ios-mcp/src/index.ts b/packages/ios-mcp/src/index.ts new file mode 100644 index 000000000..c0f4d2315 --- /dev/null +++ b/packages/ios-mcp/src/index.ts @@ -0,0 +1,19 @@ +import { parseArgs } from 'node:util'; +import { type CLIArgs, CLI_ARGS_CONFIG } from '@midscene/shared/mcp'; +import { IOSMCPServer } from './server.js'; + +const { values } = parseArgs({ options: CLI_ARGS_CONFIG }); +const args = values as CLIArgs; + +const server = new IOSMCPServer(); + +if (args.mode === 'http') { + server + .launchHttp({ + port: Number.parseInt(args.port || '3000', 10), + host: args.host || 'localhost', + }) + .catch(console.error); +} else { + server.launch().catch(console.error); +} diff --git a/packages/ios-mcp/src/ios-tools.ts b/packages/ios-mcp/src/ios-tools.ts new file mode 100644 index 000000000..4d2c3abcc --- /dev/null +++ b/packages/ios-mcp/src/ios-tools.ts @@ -0,0 +1,89 @@ +import { z } from '@midscene/core'; +import { type IOSAgent, agentFromWebDriverAgent } from '@midscene/ios'; +import { getDebug } from '@midscene/shared/logger'; +import { + BaseMidsceneTools, + type ToolDefinition, + defaultAppLoadingCheckIntervalMs, + defaultAppLoadingTimeoutMs, +} from '@midscene/shared/mcp'; + +const debug = getDebug('mcp:ios-tools'); + +/** + * iOS-specific tools manager + * Extends BaseMidsceneTools to provide iOS WebDriverAgent connection tools + */ +export class IOSMidsceneTools extends BaseMidsceneTools { + protected createTemporaryDevice() { + // Use require to avoid circular dependency with @midscene/ios + const { IOSDevice } = require('@midscene/ios'); + // Create minimal temporary instance without connecting to WebDriverAgent + // The constructor only initializes WDA backend, doesn't establish connection + return new IOSDevice({}); + } + + protected async ensureAgent(): Promise { + if (this.agent) { + return this.agent; + } + + debug('Creating iOS agent with WebDriverAgent'); + this.agent = await agentFromWebDriverAgent({ + autoDismissKeyboard: false, + }); + return this.agent; + } + + /** + * Provide iOS-specific platform tools + */ + protected preparePlatformTools(): ToolDefinition[] { + return [ + { + name: 'ios_connect', + description: + 'Connect to iOS device or simulator via WebDriverAgent and optionally launch an app', + schema: { + uri: z + .string() + .optional() + .describe( + 'Optional URI to launch app (e.g., http://example.com for URL, or com.example.app for bundle ID)', + ), + }, + handler: async ({ uri }: { uri?: string }) => { + const agent = await this.ensureAgent(); + + // 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 iOS device${uri ? ` and launched: ${uri} (app ready)` : ''}`, + }, + ...this.buildScreenshotContent(screenshot), + ], + isError: false, + }; + }, + autoDestroy: false, + }, + ]; + } +} diff --git a/packages/ios-mcp/src/server.ts b/packages/ios-mcp/src/server.ts new file mode 100644 index 000000000..2dbdb59b5 --- /dev/null +++ b/packages/ios-mcp/src/server.ts @@ -0,0 +1,22 @@ +import { BaseMCPServer } from '@midscene/shared/mcp'; +import { IOSMidsceneTools } from './ios-tools.js'; + +declare const __VERSION__: string; + +/** + * iOS MCP Server + * Provides MCP tools for iOS automation through WebDriverAgent + */ +export class IOSMCPServer extends BaseMCPServer { + constructor() { + super({ + name: '@midscene/ios-mcp', + version: __VERSION__, + description: 'Midscene MCP Server for iOS automation', + }); + } + + protected createToolsManager(): IOSMidsceneTools { + return new IOSMidsceneTools(); + } +} diff --git a/packages/ios-mcp/tests/http-server.test.ts b/packages/ios-mcp/tests/http-server.test.ts new file mode 100644 index 000000000..8991c1b1f --- /dev/null +++ b/packages/ios-mcp/tests/http-server.test.ts @@ -0,0 +1,99 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { IOSMCPServer } from '../src/server.js'; + +describe('IOSMCPServer HTTP mode', () => { + let server: IOSMCPServer; + const testPort = 13581; // Use different port than other MCP servers + const testHost = '127.0.0.1'; // Use IPv4 explicitly to avoid IPv6 issues in CI + + beforeAll(async () => { + server = new IOSMCPServer(); + }); + + afterAll(async () => { + // Cleanup will be handled by process exit + }); + + it('should start HTTP server successfully', async () => { + // Mock process.exit to prevent test exit + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit called'); + }) as any); + + try { + // Start server in background and handle potential errors + const serverPromise = server.launchHttp({ + port: testPort, + host: testHost, + }); + + // Catch any errors from server startup without blocking + serverPromise.catch((error) => { + console.error('Server startup error:', error); + }); + + // Wait for server to start with retries (up to 5 seconds) + let connected = false; + for (let i = 0; i < 10; i++) { + await new Promise((resolve) => setTimeout(resolve, 500)); + try { + const response = await fetch(`http://${testHost}:${testPort}/mcp`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'initialize', + params: { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { + name: 'test-client', + version: '1.0.0', + }, + }, + id: 1, + }), + }); + + // Server should respond (even if initialization fails without device) + expect(response.status).toBeGreaterThanOrEqual(200); + expect(response.status).toBeLessThan(600); + connected = true; + console.log( + `✓ iOS MCP server started and responding (attempt ${i + 1})`, + ); + break; + } catch (error) { + if (i === 9) { + throw error; // Throw on last attempt + } + // Otherwise continue retrying + } + } + + expect(connected).toBe(true); + } finally { + exitSpy.mockRestore(); + } + }, 15000); // Increase timeout for CI + + it('should reject invalid port numbers', async () => { + const invalidServer = new IOSMCPServer(); + + await expect( + invalidServer.launchHttp({ + port: -1, + host: testHost, + }), + ).rejects.toThrow(/Invalid port number/); + + await expect( + invalidServer.launchHttp({ + port: 99999, + host: testHost, + }), + ).rejects.toThrow(/Invalid port number/); + }); +}); diff --git a/packages/ios-mcp/tsconfig.json b/packages/ios-mcp/tsconfig.json new file mode 100644 index 000000000..83e2763d7 --- /dev/null +++ b/packages/ios-mcp/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../shared/tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2021"], + "noEmit": true, + "useDefineForClassFields": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/packages/ios-mcp/vitest.config.ts b/packages/ios-mcp/vitest.config.ts new file mode 100644 index 000000000..7c747ac04 --- /dev/null +++ b/packages/ios-mcp/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; +import { version } from './package.json'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + define: { + __VERSION__: JSON.stringify(version), + }, + ssr: { + external: ['@silvia-odwyer/photon'], + }, +}); diff --git a/packages/ios-playground/package.json b/packages/ios-playground/package.json index c836e9c0e..976e44147 100644 --- a/packages/ios-playground/package.json +++ b/packages/ios-playground/package.json @@ -20,7 +20,7 @@ "@midscene/ios": "workspace:*" }, "devDependencies": { - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", "@types/node": "^18.0.0", "typescript": "^5.8.3" }, diff --git a/packages/ios/package.json b/packages/ios/package.json index 341ea9a88..ed06ae752 100644 --- a/packages/ios/package.json +++ b/packages/ios/package.json @@ -49,7 +49,7 @@ }, "devDependencies": { "@midscene/playground": "workspace:*", - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", "@types/node": "^18.0.0", "dotenv": "^16.4.5", "typescript": "^5.8.3", diff --git a/packages/ios/src/device.ts b/packages/ios/src/device.ts index bc6a78093..5d98aadf8 100644 --- a/packages/ios/src/device.ts +++ b/packages/ios/src/device.ts @@ -33,7 +33,6 @@ import { IOSWebDriverClient as WebDriverAgentBackend } from './ios-webdriver-cli export type { IOSDeviceOpt, IOSDeviceInputOpt } from '@midscene/core/device'; const debugDevice = getDebug('ios:device'); -const BackspaceChar = '\u0008'; // Unicode backspace character /** * HTTP methods supported by WebDriverAgent API diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 0fbab5ed9..794d360bf 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,16 +1,29 @@ { "name": "@midscene/mcp", "version": "1.0.0", + "description": "Deprecated - Use @midscene/web-bridge-mcp, @midscene/android-mcp, or @midscene/ios-mcp", "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" }, + "dependencies": {}, "devDependencies": { "@midscene/android": "workspace:*", "@midscene/core": "workspace:*", @@ -19,20 +32,9 @@ "@midscene/web": "workspace:*", "@modelcontextprotocol/inspector": "^0.16.3", "@modelcontextprotocol/sdk": "1.10.2", - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", "@types/node": "^18.0.0", - "dotenv": "^16.4.5", - "puppeteer-core": "24.2.0", - "typescript": "^5.8.3", - "vitest": "3.0.5", - "zod": "3.24.3" - }, - "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" + "typescript": "^5.8.3" }, "license": "MIT" } diff --git a/packages/mcp/rslib.config.ts b/packages/mcp/rslib.config.ts index 947fc12bb..ef08f1172 100644 --- a/packages/mcp/rslib.config.ts +++ b/packages/mcp/rslib.config.ts @@ -1,4 +1,3 @@ -import path from 'node:path'; import { defineConfig } from '@rslib/core'; import { version } from './package.json'; @@ -9,23 +8,9 @@ export default defineConfig({ }, 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', - ], - }, lib: [ { format: 'cjs', diff --git a/packages/mcp/src/deprecation-tools.ts b/packages/mcp/src/deprecation-tools.ts new file mode 100644 index 000000000..a7ab87e22 --- /dev/null +++ b/packages/mcp/src/deprecation-tools.ts @@ -0,0 +1,217 @@ +import { z } from '@midscene/core'; +import { + type BaseAgent, + BaseMidsceneTools, + type ToolDefinition, +} from '@midscene/shared/mcp'; + +const DEPRECATION_MESSAGE = ` +⚠️ DEPRECATION NOTICE ⚠️ + +The @midscene/mcp package is deprecated and no longer maintained. + +Please migrate to one of the platform-specific MCP packages: + • @midscene/web-bridge-mcp - For web browser automation (Bridge mode) + • @midscene/android-mcp - For Android device automation + • @midscene/ios-mcp - For iOS device automation + +These new packages provide better performance, stability, and platform-specific features. + +Migration Guide: +1. Uninstall @midscene/mcp +2. Install the appropriate platform-specific package +3. Update your MCP configuration to use the new package name + +For more information, visit: https://midscenejs.com/mcp-migration +`; + +/** + * Mock tools definitions that will return deprecation notices + */ +export const tools = { + // Common tools + wait_for: { + name: 'wait_for', + description: + 'DEPRECATED: Waits until a specified condition, described in natural language, becomes true on the page. Use platform-specific MCP packages instead.', + }, + assert: { + name: 'assert', + description: + 'DEPRECATED: Asserts that a specified condition, described in natural language, is true on the page. Use platform-specific MCP packages instead.', + }, + take_screenshot: { + name: 'take_screenshot', + description: + 'DEPRECATED: Captures a screenshot of the currently active page. Use platform-specific MCP packages instead.', + }, + + // Web-specific tools + web_connect: { + name: 'web_connect', + description: + 'DEPRECATED: Connect to web page by opening new tab with URL. Use @midscene/web-bridge-mcp instead.', + }, + + // Android-specific tools + midscene_android_connect: { + name: 'midscene_android_connect', + description: + 'DEPRECATED: Connect to an Android device via ADB for automation. Use @midscene/android-mcp instead.', + }, + midscene_android_list_devices: { + name: 'midscene_android_list_devices', + description: + 'DEPRECATED: List all connected Android devices available for automation. Use @midscene/android-mcp instead.', + }, + + // iOS-specific tools + midscene_ios_connect: { + name: 'midscene_ios_connect', + description: + 'DEPRECATED: Connect to an iOS device via WebDriverAgent for automation. Use @midscene/ios-mcp instead.', + }, + midscene_ios_list_devices: { + name: 'midscene_ios_list_devices', + description: + 'DEPRECATED: List all connected iOS devices available for automation. Use @midscene/ios-mcp instead.', + }, + + // AI methods from Agent class + aiTap: { + name: 'aiTap', + description: + 'DEPRECATED: AI-powered tap/click action on elements. Use platform-specific MCP packages instead.', + }, + aiRightClick: { + name: 'aiRightClick', + description: + 'DEPRECATED: AI-powered right-click action on elements. Use platform-specific MCP packages instead.', + }, + aiDoubleClick: { + name: 'aiDoubleClick', + description: + 'DEPRECATED: AI-powered double-click action on elements. Use platform-specific MCP packages instead.', + }, + aiHover: { + name: 'aiHover', + description: + 'DEPRECATED: AI-powered hover action on elements. Use platform-specific MCP packages instead.', + }, + aiInput: { + name: 'aiInput', + description: + 'DEPRECATED: AI-powered input text into form fields. Use platform-specific MCP packages instead.', + }, + aiKeyboardPress: { + name: 'aiKeyboardPress', + description: + 'DEPRECATED: AI-powered keyboard press action. Use platform-specific MCP packages instead.', + }, + aiScroll: { + name: 'aiScroll', + description: + 'DEPRECATED: AI-powered scroll action on page or elements. Use platform-specific MCP packages instead.', + }, + aiAct: { + name: 'aiAct', + description: + 'DEPRECATED: AI-powered natural language action execution. Use platform-specific MCP packages instead.', + }, + aiAction: { + name: 'aiAction', + description: + 'DEPRECATED: Alias for aiAct. Use platform-specific MCP packages instead.', + }, + aiQuery: { + name: 'aiQuery', + description: + 'DEPRECATED: AI-powered data extraction from the page. Use platform-specific MCP packages instead.', + }, + aiBoolean: { + name: 'aiBoolean', + description: + 'DEPRECATED: AI-powered boolean query from the page. Use platform-specific MCP packages instead.', + }, + aiNumber: { + name: 'aiNumber', + description: + 'DEPRECATED: AI-powered number extraction from the page. Use platform-specific MCP packages instead.', + }, + aiString: { + name: 'aiString', + description: + 'DEPRECATED: AI-powered string extraction from the page. Use platform-specific MCP packages instead.', + }, + aiAsk: { + name: 'aiAsk', + description: + 'DEPRECATED: AI-powered question answering from the page. Use platform-specific MCP packages instead.', + }, + aiLocate: { + name: 'aiLocate', + description: + 'DEPRECATED: AI-powered element location on the page. Use platform-specific MCP packages instead.', + }, + aiAssert: { + name: 'aiAssert', + description: + 'DEPRECATED: AI-powered assertion on page state. Use platform-specific MCP packages instead.', + }, + aiWaitFor: { + name: 'aiWaitFor', + description: + 'DEPRECATED: AI-powered wait for condition to be true. Use platform-specific MCP packages instead.', + }, + ai: { + name: 'ai', + description: + 'DEPRECATED: Shorthand for aiAct. Use platform-specific MCP packages instead.', + }, +}; + +/** + * Deprecation tools manager that registers mock tools + * All tools return deprecation notices + */ +export class DeprecationMidsceneTools extends BaseMidsceneTools { + protected createTemporaryDevice() { + // Return a minimal mock device that satisfies the interface + // This device is never actually used since all tools return deprecation messages + return { + async getActionSpace() { + return []; + }, + } as any; + } + + protected async ensureAgent(_initParam?: string): Promise { + // Return a minimal mock agent + // This agent is never actually used since all tools return deprecation messages + return { + async getActionSpace() { + return []; + }, + } as BaseAgent; + } + + protected preparePlatformTools(): ToolDefinition[] { + // Convert tools object to ToolDefinition array + return Object.values(tools).map((tool) => ({ + name: tool.name, + description: tool.description, + schema: { + _deprecated: z.boolean().optional().describe('This tool is deprecated'), + }, + handler: async () => ({ + content: [ + { + type: 'text' as const, + text: DEPRECATION_MESSAGE, + }, + ], + }), + autoDestroy: false, + })); + } +} diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 2ac3de152..9df063b59 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -1,37 +1,20 @@ #!/usr/bin/env node -import { setIsMcp } from '@midscene/shared/utils'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { MidsceneTools } from './midscene.js'; - -declare const __VERSION__: string; - -setIsMcp(true); - -const server = new McpServer({ - name: '@midscene/mcp', - version: __VERSION__, - description: - 'Midscene MCP Server: Control the browser using natural language commands for navigation, clicking, input, hovering, and achieving goals. Also supports screenshots and JavaScript execution.', -}); - -let midsceneManager: MidsceneTools; - -async function runServer() { - midsceneManager = new MidsceneTools(); - - // Initialize tools asynchronously (independent of server) - await midsceneManager.initTools(); - midsceneManager.attachToServer(server); - - const transport = new StdioServerTransport(); - await server.connect(transport); +import { parseArgs } from 'node:util'; +import { type CLIArgs, CLI_ARGS_CONFIG } from '@midscene/shared/mcp'; +import { DeprecatedMCPServer } from './server.js'; + +const { values } = parseArgs({ options: CLI_ARGS_CONFIG }); +const args = values as CLIArgs; + +const server = new DeprecatedMCPServer(); + +if (args.mode === 'http') { + server + .launchHttp({ + port: Number.parseInt(args.port || '3000', 10), + host: args.host || 'localhost', + }) + .catch(console.error); +} else { + server.launch().catch(console.error); } - -runServer().catch(console.error); - -process.stdin.on('close', () => { - console.error('Midscene MCP Server closing, cleaning up browser...'); - server.close(); - midsceneManager.closeBrowser().catch(console.error); -}); diff --git a/packages/mcp/src/midscene.ts b/packages/mcp/src/midscene.ts deleted file mode 100644 index 609f558da..000000000 --- a/packages/mcp/src/midscene.ts +++ /dev/null @@ -1,479 +0,0 @@ -import { - AndroidAgent, - AndroidDevice, - getConnectedDevices, -} from '@midscene/android'; -import type { DeviceAction } from '@midscene/core'; -import { - MIDSCENE_MCP_ANDROID_MODE, - MIDSCENE_MCP_USE_PUPPETEER_MODE, - globalConfigManager, -} from '@midscene/shared/env'; -import { parseBase64 } from '@midscene/shared/img'; -import { getDebug } from '@midscene/shared/logger'; -import { AgentOverChromeBridge } from '@midscene/web/bridge-mode'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { - ImageContent, - TextContent, -} from '@modelcontextprotocol/sdk/types.js'; -import { z } from 'zod'; -import { PuppeteerBrowserAgent, ensureBrowser } from './puppeteer'; -import { tools } from './tools'; - -declare global { - interface Window { - mcpHelper?: { - logs: string[]; - originalConsole: Partial; - }; - } -} - -const debug = getDebug('mcp:tools'); - -/** - * Tool definition interface for caching prepared tool configurations - */ -interface ToolDefinition { - name: string; - description: string; - schema: any; - handler: (...args: any[]) => Promise; - autoDestroy?: boolean; // Whether to auto destroy agent after execution -} - -export class MidsceneTools { - private mcpServer?: McpServer; // Add server instance - private agent?: AgentOverChromeBridge | PuppeteerBrowserAgent | AndroidAgent; - private puppeteerMode = globalConfigManager.getEnvConfigInBoolean( - MIDSCENE_MCP_USE_PUPPETEER_MODE, - ); - private androidMode = globalConfigManager.getEnvConfigInBoolean( - MIDSCENE_MCP_ANDROID_MODE, - ); // Add Android mode flag - private androidDeviceId?: string; // Add device ID storage - private toolDefinitions: ToolDefinition[] = []; // Store all tool definitions - - /** - * Attach this manager to an MCP server instance and register all tools. - * This method must be called after initTools. - */ - public attachToServer(server: McpServer): void { - this.mcpServer = server; - - if (this.toolDefinitions.length === 0) { - throw new Error( - 'No tool definitions found. Call initTools() before attachToServer().', - ); - } - - // Register all cached tool definitions - for (const toolDef of this.toolDefinitions) { - if (toolDef.autoDestroy) { - this.toolWithAutoDestroy( - toolDef.name, - toolDef.description, - toolDef.schema, - toolDef.handler, - ); - } else { - // Register without auto-destroy wrapper - this.mcpServer.tool( - toolDef.name, - toolDef.description, - toolDef.schema, - toolDef.handler, - ); - } - } - - debug('Registered', this.toolDefinitions.length, 'tools with MCP server'); - } - - // initializes or re-initializes the browser agent. - private async ensureAgent(openNewTabWithUrl?: string) { - // re-init the agent if url is provided - if (this.agent && openNewTabWithUrl) { - try { - await this.agent.destroy(); - } catch (e) { - // console.error('failed to destroy agent', e); - } - this.agent = undefined; - } - - if (this.agent) return this.agent; - - // Check if running in Android mode or bridge mode - if (this.androidMode) { - this.agent = await this.initAndroidAgent(openNewTabWithUrl); - } else if (!this.puppeteerMode) { - this.agent = await this.initAgentByBridgeMode(openNewTabWithUrl); - } else { - this.agent = await this.initPuppeteerAgent(openNewTabWithUrl); - } - - return this.agent; - } - - private async initAgentByBridgeMode( - openNewTabWithUrl?: string, - ): Promise { - let agent: AgentOverChromeBridge; - try { - // Create a new agent instance designed for bridge mode. - agent = new AgentOverChromeBridge({ - closeConflictServer: true, - }); - // If this is the first initialization (not re-init), - if (!openNewTabWithUrl) { - // Connect the agent to the currently active tab in the browser. - await agent.connectCurrentTab(); - } else { - await agent.connectNewTabWithUrl(openNewTabWithUrl); - } - return agent; - } catch (err) { - //@ts-ignore - if (agent) { - await agent.destroy(); - } - console.error('Bridge mode connection failed', err); - // Check if we've exceeded the maximum retry attempts - throw new Error( - 'Unable to establish Bridge mode connection. Please check the following issues:\n' + - '1. Confirm Chrome browser is running\n' + - '2. Midscene extension is properly installed in Chrome\n' + - '3. Bridge mode is enabled in the extension settings\n' + - '4. No other MCP clients are using the Midscene MCP server', - ); - } - } - - private async initPuppeteerAgent(openNewTabWithUrl?: string) { - // If not in bridge mode, use Puppeteer to control a browser instance. - // Ensure a Puppeteer browser instance is running and get its details. - const { browser } = await ensureBrowser({}); - // Create a new, blank page (tab) in the browser. - const newPage = await browser.newPage(); - - // Navigate the new page to Google as a starting point. - if (openNewTabWithUrl) { - await newPage.goto(openNewTabWithUrl); - } else { - await newPage.goto('https://google.com'); - } - // Create a new Puppeteer-specific agent instance, controlling the browser and the new page. - const agent = new PuppeteerBrowserAgent(browser, newPage); - return agent; - } - - private async initAndroidAgent(uri?: string): Promise { - try { - let deviceId = this.androidDeviceId; - - // If no device ID is specified, get the first connected device - if (!deviceId) { - const devices = await getConnectedDevices(); - if (devices.length === 0) { - throw new Error( - 'No Android devices connected. Please connect a device via ADB.', - ); - } - deviceId = devices[0].udid; - this.androidDeviceId = deviceId; - } - - // Create an Android device instance - const androidDevice = new AndroidDevice(deviceId, { - autoDismissKeyboard: true, - imeStrategy: 'yadb-for-non-ascii', - }); - - // Connect to the device - await androidDevice.connect(); - - // If a URI is provided, launch the app or web page - if (uri) { - await androidDevice.launch(uri); - } - - // Create an Android Agent - const agent = new AndroidAgent(androidDevice, { - aiActionContext: - 'If any permission dialog appears, click Allow. If login page appears, close it.', - }); - - return agent; - } catch (err) { - console.error('Android mode connection failed', err); - throw new Error( - 'Unable to establish Android connection. Please check the following:\n' + - '1. Android device is connected via ADB\n' + - '2. USB debugging is enabled on the device\n' + - '3. Device is unlocked and authorized for debugging\n' + - '4. ADB is properly installed and accessible', - ); - } - } - - /** - * Prepare Android-specific tool definitions - * This method creates tool definitions that are specific to Android automation - */ - private prepareAndroidToolDefinitions(): ToolDefinition[] { - return [ - // Android device connection tool - { - name: 'midscene_android_connect', - description: 'Connect to an Android device via ADB', - schema: { - deviceId: z - .string() - .optional() - .describe( - 'Device ID to connect to. If not provided, uses the first available device.', - ), - }, - handler: async ({ deviceId }) => { - this.androidDeviceId = deviceId; - this.agent = undefined; // Reset the agent to force reinitialization - await this.ensureAgent(); - return { - content: [ - { - type: 'text', - text: `Connected to Android device: ${this.androidDeviceId}`, - }, - ], - isError: false, - }; - }, - autoDestroy: true, - }, - // Android device list tool - { - name: 'midscene_android_list_devices', - description: 'List all connected Android devices', - schema: {}, - handler: async () => { - const devices = await getConnectedDevices(); - return { - content: [ - { - type: 'text', - text: `Connected Android devices:\n${JSON.stringify(devices, null, 2)}`, - }, - ], - isError: false, - }; - }, - autoDestroy: false, // No agent needed, no auto destroy - }, - ]; - } - - /** - * Prepare common tool definitions (aiWaitFor, aiAssert, screenshot) - */ - private prepareCommonToolDefinitions(): ToolDefinition[] { - return [ - // aiWaitFor tool - { - name: tools.wait_for.name, - description: tools.wait_for.description, - schema: { - assertion: z - .string() - .describe( - 'Condition to monitor on the page, described in natural language.', - ), - timeoutMs: z - .number() - .optional() - .default(15000) - .describe('Maximum time to wait (ms).\nDefault: 15000'), - checkIntervalMs: z - .number() - .optional() - .default(3000) - .describe('How often to check the condition (ms).\nDefault: 3000'), - }, - handler: async ({ assertion, timeoutMs, checkIntervalMs }) => { - const agent = await this.ensureAgent(); - await agent.aiWaitFor(assertion, { - timeoutMs, - checkIntervalMs, - }); - return { - content: [ - { type: 'text', text: `Wait condition met: "${assertion}"` }, - ], - }; - }, - autoDestroy: true, - }, - // aiAssert tool - { - name: tools.assert.name, - description: tools.assert.description, - schema: { - assertion: z - .string() - .describe( - 'Condition to monitor on the page, described in natural language.', - ), - }, - handler: async ({ assertion }) => { - const agent = await this.ensureAgent(); - await agent.aiAssert(assertion); - return { - content: [ - { type: 'text', text: `Assert condition : "${assertion}"` }, - ], - }; - }, - autoDestroy: true, - }, - // Screenshot tool - { - name: tools.take_screenshot.name, - description: tools.take_screenshot.description, - schema: {}, - handler: async () => { - const agent = await this.ensureAgent(); - const screenshot = await agent.page.screenshotBase64(); - - const { mimeType, body } = parseBase64(screenshot); - - return { - content: [ - { - type: 'image', - data: body, - mimeType, - } as ImageContent, - ], - isError: false, - }; - }, - autoDestroy: true, - }, - ]; - } - - /** - * Prepare dynamic action space tool definitions - */ - private prepareActionSpaceToolDefinitions( - actionSpace: DeviceAction[], - ): ToolDefinition[] { - const tools = actionSpace.map((action) => ({ - name: action.name, - description: `Ask Midscene (a helper that can understand natural language and perform actions) to perform the action "${action.name}", this action is defined as follows: ${action.description || 'No description provided'}.`, - schema: { - instruction: z - .string() - .describe('The detailed instruction on how to perform the action'), - }, - handler: async ({ instruction }: { instruction: string }) => { - const agent = await this.ensureAgent(); - await agent.aiAct( - `Use the action "${action.name}" to do this: ${instruction}`, - ); - const screenshot = await agent.page.screenshotBase64(); - const { mimeType, body } = parseBase64(screenshot); - return { - content: [ - { - type: 'text', - text: `Action performed, the report is: ${agent.reportFile} , and i will give you the screenshot after taking it`, - } as TextContent, - { - type: 'image', - data: body, - mimeType, - } as ImageContent, - ], - isError: false, - }; - }, - autoDestroy: true, - })); - return tools; - } - - /** - * Initialize tools by preparing all tool definitions. - * This method is async and should be called before registerTools. - * It's independent of the MCP server. - */ - public async initTools() { - // Clear existing definitions - this.toolDefinitions = []; - - // Prepare Android tools if in Android mode - if (this.androidMode) { - const androidTools = this.prepareAndroidToolDefinitions(); - this.toolDefinitions.push(...androidTools); - } - - // Prepare dynamic action space tools - const agent = await this.ensureAgent(); - const actionSpace = await agent.getActionSpace(); - const actionTools = this.prepareActionSpaceToolDefinitions(actionSpace); - this.toolDefinitions.push(...actionTools); - - // Prepare common tools - const commonTools = this.prepareCommonToolDefinitions(); - this.toolDefinitions.push(...commonTools); - - // List all the tools in the toolDefinitions array - debug( - 'Tool definitions:', - this.toolDefinitions.map((tool) => ({ - name: tool.name, - description: tool.description, - })), - ); - debug('Total tool definitions prepared:', this.toolDefinitions.length); - } - - public async closeBrowser(): Promise { - await this.agent?.destroy(); - } - - /** - * Wrapper for tool registration that automatically destroys the agent after each tool call. - * This ensures each tool call starts with a fresh agent instance and prevents connection leaks. - * - * Usage: Replace `this.mcpServer.tool(...)` with `this.toolWithAutoDestroy(...)` - */ - private toolWithAutoDestroy( - name: string, - description: string, - schema: any, - handler: (...args: any[]) => Promise, - ) { - if (!this.mcpServer) { - throw new Error('MCP server not attached'); - } - this.mcpServer.tool(name, description, schema, async (...args: any[]) => { - try { - return await handler(...args); - } finally { - // Always destroy agent after tool execution - if (!process.env.MIDSCENE_MCP_DISABLE_AGENT_AUTO_DESTROY) { - try { - await this.agent?.destroy(); - } catch (e) { - // Ignore destroy errors to prevent them from masking the actual result - // console.error('Error destroying agent:', e); - } - this.agent = undefined; - } - } - }); - } -} diff --git a/packages/mcp/src/puppeteer.ts b/packages/mcp/src/puppeteer.ts deleted file mode 100644 index efa334bec..000000000 --- a/packages/mcp/src/puppeteer.ts +++ /dev/null @@ -1,204 +0,0 @@ -// fork from https://github.com/modelcontextprotocol/servers/blob/f93737dbb098f8c078365c63c94908598f7db157/src/puppeteer/index.ts - -import { PuppeteerAgent } from '@midscene/web/puppeteer'; -import type { Browser, LaunchOptions } from 'puppeteer-core'; -import type { Page } from 'puppeteer-core'; -import puppeteer from 'puppeteer-core'; -import { deepMerge, getChromePathFromEnv } from './utils'; - -// Global state -let browser: Browser | null; -let page: Page | null; -const consoleLogs: string[] = []; -let previousLaunchOptions: any = null; - -const DANGEROUS_ARGS = [ - '--no-sandbox', - '--disable-setuid-sandbox', - '--single-process', - '--disable-web-security', - '--ignore-certificate-errors', - '--disable-features=IsolateOrigins', - '--disable-site-isolation-trials', - '--allow-running-insecure-content', -]; - -function getBrowserLaunchOptions( - launchOptions: LaunchOptions | undefined, - allowDangerous: boolean | undefined, -): LaunchOptions { - // Parse environment config safely - let envConfig = {}; - try { - envConfig = JSON.parse(process.env.PUPPETEER_LAUNCH_OPTIONS || '{}'); - } catch (error: any) { - console.warn( - 'Failed to parse PUPPETEER_LAUNCH_OPTIONS:', - error?.message || error, - ); - } - - // Deep merge environment config with user-provided options - const mergedConfig = deepMerge(envConfig, launchOptions || {}); - - // Security validation for merged config - if (mergedConfig?.args) { - const dangerousArgs = mergedConfig.args?.filter?.((arg: string) => - DANGEROUS_ARGS.some((dangerousArg: string) => - arg.startsWith(dangerousArg), - ), - ); - if ( - dangerousArgs?.length > 0 && - !(allowDangerous || process.env.ALLOW_DANGEROUS === 'true') - ) { - throw new Error( - `Dangerous browser arguments detected: ${dangerousArgs.join(', ')}. Found from environment variable and tool call argument. Set allowDangerous: true in the tool call arguments to override.`, - ); - } - } - - const systemChromePath = getChromePathFromEnv(); - const npx_args = { - headless: false, - defaultViewport: null, - args: ['--window-size=1920,1080'], - ...(systemChromePath && { executablePath: systemChromePath }), - }; - const docker_args = { - headless: true, - args: ['--no-sandbox', '--single-process', '--no-zygote'], - ...(systemChromePath && { executablePath: systemChromePath }), - }; - - return deepMerge( - process.env.DOCKER_CONTAINER === 'true' ? docker_args : npx_args, - mergedConfig, - ); -} - -async function ensureBrowser({ launchOptions, allowDangerous }: any) { - const currentLaunchOptions = getBrowserLaunchOptions( - launchOptions, - allowDangerous, - ); - - try { - if ( - (browser && !browser.connected) || - JSON.stringify(currentLaunchOptions) !== - JSON.stringify(previousLaunchOptions) - ) { - await browser?.close(); - browser = null; - } - } catch (error) { - console.warn('Error checking or closing existing browser:', error); - browser = null; - } - - if (!browser) { - previousLaunchOptions = currentLaunchOptions; - browser = await puppeteer.launch(currentLaunchOptions); - const pages = await browser.pages(); - page = pages[0]; - consoleLogs.length = 0; // Clear logs for new browser session - - return { - browser, - pages, - }; - } - const pages = await browser.pages(); - return { - browser, - pages, - }; -} - -export { ensureBrowser }; - -// Class to encapsulate Puppeteer browser operations -export class PuppeteerBrowserAgent extends PuppeteerAgent { - private browser: Browser; - - constructor(browser: Browser, page: Page) { - // @ts-expect-error The `Page` type in Puppeteer and Puppeteer-core is the same, but it is in different files. They all have a `#private` declare, which causes a TypeScript error. - super(page); - this.browser = browser; - } - - async connectNewTabWithUrl(url: string): Promise { - await this.page.navigate(url); - } - - /** - * In headful mode, find the uniquely visible Page in the current window. - * In headless mode, all pages are considered visible and cannot be distinguished. - */ - async getActivePage(timeout = 2000): Promise { - const t0 = Date.now(); - while (Date.now() - t0 < timeout) { - const pages = await this.browser.pages(); - const visible = []; - - for (const p of pages) { - // @ts-ignore - const state = await p.evaluate(() => document.visibilityState); - if (state === 'visible') visible.push(p); - } - if (visible.length === 1) return visible[0]; // Typically returns only one - await new Promise((r) => setTimeout(r, 100)); // Wait a bit before trying again - } - throw new Error('Unable to determine the currently active tab'); - } - - /** - * Obtain all TAB page information through puppeteer - * @returns {Promise>} - */ - async getBrowserTabList(): Promise< - { url: string; title: string; id: string; currentActiveTab: boolean }[] - > { - const pages = await this.browser.pages(); - // Ensure getActivePage is called correctly within the class context - const activePage = await this.getActivePage(); - const tabsInfo = await Promise.all( - pages.map(async (page: Page) => ({ - url: page.url(), - title: await page.title(), - id: `${(page.mainFrame() as any)._id}`, - currentActiveTab: - activePage && - (page.mainFrame() as any)._id === (activePage.mainFrame() as any)._id, - })), - ); - - // Filter out tabs where essential info might be missing (e.g., about:blank initially) - return tabsInfo.filter((tab) => tab.url && tab.title && tab.id); - } - - /** - * Sets the specified tab as the active tab in the browser window. - * Uses page.bringToFront() for activation. - * @param tabId The ID of the tab to activate (obtained from getTabsWithPuppeteer). - * @returns {Promise} True if the tab was found and activated, false otherwise. - */ - async setActiveTabId(tabId: string): Promise { - const pages = await this.browser.pages(); - for (const page of pages) { - const currentPageId = `${(page.mainFrame() as any)._id}`; - if (currentPageId === tabId) { - try { - await page.bringToFront(); - return true; // Tab found and activated - } catch (error) { - console.error(`Error bringing tab ${tabId} to front:`, error); - return false; // Error during activation - } - } - } - console.warn(`setActiveTab: Tab with ID '${tabId}' not found.`); - return false; // Tab not found - } -} diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts new file mode 100644 index 000000000..b450061ee --- /dev/null +++ b/packages/mcp/src/server.ts @@ -0,0 +1,26 @@ +import { BaseMCPServer } from '@midscene/shared/mcp'; +import { DeprecationMidsceneTools } from './deprecation-tools.js'; + +declare const __VERSION__: string; + +/** + * Deprecated MCP Server class + * This package is deprecated. Please use platform-specific packages instead: + * - @midscene/web-bridge-mcp for web automation + * - @midscene/android-mcp for Android automation + * - @midscene/ios-mcp for iOS automation + */ +export class DeprecatedMCPServer extends BaseMCPServer { + constructor() { + super({ + name: '@midscene/mcp', + version: __VERSION__, + description: + 'Deprecated - Use @midscene/web-bridge-mcp, @midscene/android-mcp, or @midscene/ios-mcp', + }); + } + + protected createToolsManager(): DeprecationMidsceneTools { + return new DeprecationMidsceneTools(); + } +} diff --git a/packages/mcp/src/tools.ts b/packages/mcp/src/tools.ts deleted file mode 100644 index 1a29acd43..000000000 --- a/packages/mcp/src/tools.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const tools = { - // Common tools - wait_for: { - name: 'wait_for', - description: - 'Waits until a specified condition, described in natural language, becomes true on the page. Polls the condition using AI.', - }, - assert: { - name: 'assert', - description: - 'Asserts that a specified condition, described in natural language, is true on the page. Polls the condition using AI.', - }, - take_screenshot: { - name: 'take_screenshot', - description: - 'Captures a screenshot of the currently active browser tab and saves it with the given name.', - }, - // Android-specific tools - midscene_android_connect: { - name: 'midscene_android_connect', - description: 'Connect to an Android device via ADB for automation', - }, - midscene_android_list_devices: { - name: 'midscene_android_list_devices', - description: 'List all connected Android devices available for automation', - }, -}; diff --git a/packages/mcp/tests/__snapshots__/index.test.ts.snap b/packages/mcp/tests/__snapshots__/index.test.ts.snap deleted file mode 100644 index f8c85672f..000000000 --- a/packages/mcp/tests/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Tools Module > should have all expected tools defined 1`] = ` -{ - "assert": { - "description": "Asserts that a specified condition, described in natural language, is true on the page. Polls the condition using AI.", - "name": "assert", - }, - "midscene_android_connect": { - "description": "Connect to an Android device via ADB for automation", - "name": "midscene_android_connect", - }, - "midscene_android_list_devices": { - "description": "List all connected Android devices available for automation", - "name": "midscene_android_list_devices", - }, - "take_screenshot": { - "description": "Captures a screenshot of the currently active browser tab and saves it with the given name.", - "name": "take_screenshot", - }, - "wait_for": { - "description": "Waits until a specified condition, described in natural language, becomes true on the page. Polls the condition using AI.", - "name": "wait_for", - }, -} -`; diff --git a/packages/mcp/tests/puppeteer.test.ts b/packages/mcp/tests/puppeteer.test.ts deleted file mode 100644 index 5b74eb46d..000000000 --- a/packages/mcp/tests/puppeteer.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; -import { ensureBrowser } from '../src/puppeteer'; - -// Mock external dependencies -vi.mock('puppeteer-core', () => ({ - default: { - launch: vi.fn(), - }, -})); - -vi.mock('@midscene/web/puppeteer', () => ({ - PuppeteerAgent: vi.fn(), -})); - -vi.mock('../src/utils', () => ({ - deepMerge: vi.fn((target, source) => ({ ...target, ...source })), - getChromePathFromEnv: vi.fn(() => '/mock/chrome/path'), -})); - -// Mock browser and page objects -const mockPage = { - evaluate: vi.fn(), - navigate: vi.fn(), - url: vi.fn(() => 'https://example.com'), - title: vi.fn().mockResolvedValue('Test Page'), - mainFrame: vi.fn(() => ({ _id: 'frame-123' })), - bringToFront: vi.fn(), -}; - -const mockBrowser = { - connected: true, - close: vi.fn(), - pages: vi.fn().mockResolvedValue([mockPage]), - newPage: vi.fn().mockResolvedValue(mockPage), -} as any; - -describe('Puppeteer Module', () => { - let consoleWarnSpy: any; - - beforeEach(() => { - vi.clearAllMocks(); - // Mock console.warn to suppress expected error messages in tests - consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - // Reset environment - process.env.DOCKER_CONTAINER = undefined; - process.env.PUPPETEER_LAUNCH_OPTIONS = undefined; - process.env.ALLOW_DANGEROUS = undefined; - }); - - afterEach(() => { - consoleWarnSpy?.mockRestore(); - }); - - describe('ensureBrowser', () => { - test('should launch browser with default options', async () => { - const puppeteer = await import('puppeteer-core'); - vi.mocked(puppeteer.default.launch).mockResolvedValue(mockBrowser); - mockBrowser.pages.mockResolvedValue([mockPage]); - - const result = await ensureBrowser({}); - - expect(puppeteer.default.launch).toHaveBeenCalled(); - expect(result.browser).toBe(mockBrowser); - expect(result.pages).toEqual([mockPage]); - }); - - test('should use Docker options when in Docker container', async () => { - process.env.DOCKER_CONTAINER = 'true'; - const puppeteer = await import('puppeteer-core'); - vi.mocked(puppeteer.default.launch).mockResolvedValue(mockBrowser); - - await ensureBrowser({}); - - expect(puppeteer.default.launch).toHaveBeenCalled(); - const launchCall = vi.mocked(puppeteer.default.launch).mock.calls[0]?.[0]; - expect(launchCall?.headless).toBe(true); - expect(launchCall?.args).toContain('--no-sandbox'); - }); - - test('should handle invalid JSON in environment options', async () => { - process.env.PUPPETEER_LAUNCH_OPTIONS = 'invalid json'; - const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const puppeteer = await import('puppeteer-core'); - vi.mocked(puppeteer.default.launch).mockResolvedValue(mockBrowser); - - await ensureBrowser({}); - - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to parse PUPPETEER_LAUNCH_OPTIONS:', - expect.any(String), - ); - consoleSpy.mockRestore(); - }); - - test('should throw error for dangerous args without permission', async () => { - const launchOptions = { - args: ['--no-sandbox', '--disable-web-security'], - }; - - await expect(ensureBrowser({ launchOptions })).rejects.toThrow( - 'Dangerous browser arguments detected', - ); - }); - - test('should allow dangerous args with allowDangerous flag', async () => { - const launchOptions = { - args: ['--no-sandbox', '--disable-web-security'], - }; - const puppeteer = await import('puppeteer-core'); - vi.mocked(puppeteer.default.launch).mockResolvedValue(mockBrowser); - - await ensureBrowser({ launchOptions, allowDangerous: true }); - - expect(puppeteer.default.launch).toHaveBeenCalled(); - }); - - test('should reuse existing connected browser', async () => { - const puppeteer = await import('puppeteer-core'); - vi.mocked(puppeteer.default.launch).mockResolvedValue(mockBrowser); - - // First call - await ensureBrowser({}); - // Second call with same options - await ensureBrowser({}); - - expect(puppeteer.default.launch).toHaveBeenCalledTimes(1); - }); - - test('should handle environment chrome path configuration', async () => { - const { getChromePathFromEnv } = await import('../src/utils'); - vi.mocked(getChromePathFromEnv).mockReturnValue('/custom/chrome'); - - const puppeteer = await import('puppeteer-core'); - vi.mocked(puppeteer.default.launch).mockResolvedValue(mockBrowser); - - await ensureBrowser({}); - - expect(puppeteer.default.launch).toHaveBeenCalled(); - const launchCall = vi.mocked(puppeteer.default.launch).mock.calls[0]?.[0]; - expect(launchCall?.executablePath).toBe('/custom/chrome'); - }); - - test('should call deepMerge for configuration merging', async () => { - const { deepMerge } = await import('../src/utils'); - const puppeteer = await import('puppeteer-core'); - vi.mocked(puppeteer.default.launch).mockResolvedValue(mockBrowser); - - const customOptions = { headless: false }; - const result = await ensureBrowser({ launchOptions: customOptions }); - - // deepMerge should be called during configuration processing - expect(deepMerge).toHaveBeenCalled(); - expect(result.browser).toBeDefined(); - }); - - test('should validate dangerous arguments', async () => { - const dangerousOptions = { - args: ['--disable-web-security', '--ignore-certificate-errors'], - }; - - await expect( - ensureBrowser({ launchOptions: dangerousOptions }), - ).rejects.toThrow('Dangerous browser arguments detected'); - }); - - test('should use environment ALLOW_DANGEROUS flag', async () => { - process.env.ALLOW_DANGEROUS = 'true'; - const dangerousOptions = { - args: ['--no-sandbox'], - }; - const puppeteer = await import('puppeteer-core'); - vi.mocked(puppeteer.default.launch).mockResolvedValue(mockBrowser); - - await expect( - ensureBrowser({ launchOptions: dangerousOptions }), - ).resolves.not.toThrow(); - - expect(puppeteer.default.launch).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/mcp/vitest.config.ts b/packages/mcp/vitest.config.ts deleted file mode 100644 index e83eaf4c5..000000000 --- a/packages/mcp/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - // Configure Vitest (https://vitest.dev/config/) - test: {}, -}); diff --git a/packages/playground/package.json b/packages/playground/package.json index d347de140..6c2604941 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -34,7 +34,7 @@ "uuid": "11.1.0" }, "devDependencies": { - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node": "^18.0.0", diff --git a/packages/recorder/package.json b/packages/recorder/package.json index 2837a295d..501014511 100644 --- a/packages/recorder/package.json +++ b/packages/recorder/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@rsbuild/plugin-react": "^1.4.1", - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", "@types/react": "^18.3.1", "react": "18.3.1", "typescript": "^5.8.3" diff --git a/packages/shared/package.json b/packages/shared/package.json index c114500bf..7f0b0aa28 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -57,6 +57,11 @@ "import": "./dist/es/common.mjs", "require": "./dist/lib/common.js" }, + "./mcp": { + "types": "./dist/types/mcp/index.d.ts", + "import": "./dist/es/mcp/index.mjs", + "require": "./dist/lib/mcp/index.js" + }, "./*": { "types": "./dist/types/*.d.ts", "import": "./dist/es/*.mjs", @@ -78,21 +83,25 @@ "@silvia-odwyer/photon": "0.3.3", "@silvia-odwyer/photon-node": "0.3.3", "debug": "4.4.0", + "express": "^4.21.2", "jimp": "0.22.12", "js-sha256": "0.11.0", "sharp": "^0.34.3", "uuid": "11.1.0" }, "devDependencies": { - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", + "@modelcontextprotocol/sdk": "1.10.2", "@types/debug": "4.1.12", + "@types/express": "^4.17.21", "@types/node": "^18.0.0", "@ui-tars/shared": "1.2.0", "dotenv": "^16.4.5", "openai": "6.3.0", "rimraf": "~3.0.2", "typescript": "^5.8.3", - "vitest": "3.0.5" + "vitest": "3.0.5", + "zod": "3.24.3" }, "sideEffects": [], "publishConfig": { diff --git a/packages/shared/src/mcp/base-server.ts b/packages/shared/src/mcp/base-server.ts new file mode 100644 index 000000000..165ec0fc5 --- /dev/null +++ b/packages/shared/src/mcp/base-server.ts @@ -0,0 +1,404 @@ +import { randomUUID } from 'node:crypto'; +import type { ParseArgsConfig } from 'node:util'; +import { setIsMcp } from '@midscene/shared/utils'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { Application, Request, Response } from 'express'; +import type { IMidsceneTools } from './types'; + +export interface BaseMCPServerConfig { + name: string; + version: string; + description: string; +} + +export interface HttpLaunchOptions { + port: number; + host?: string; +} + +interface SessionData { + transport: StreamableHTTPServerTransport; + createdAt: Date; + lastAccessedAt: Date; +} + +/** + * CLI argument configuration for MCP servers + */ +export const CLI_ARGS_CONFIG: ParseArgsConfig['options'] = { + mode: { type: 'string', default: 'stdio' }, + port: { type: 'string', default: '3000' }, + host: { type: 'string', default: 'localhost' }, +}; + +export interface CLIArgs { + mode?: string; + port?: string; + host?: string; +} + +/** + * Launch an MCP server based on CLI arguments + * Shared helper to reduce duplication across platform CLI entry points + */ +export function launchMCPServer( + server: BaseMCPServer, + args: CLIArgs, +): Promise { + if (args.mode === 'http') { + return server.launchHttp({ + port: Number.parseInt(args.port || '3000', 10), + host: args.host || 'localhost', + }); + } + return server.launch(); +} + +const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes +const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes +const MAX_SESSIONS = 100; // Maximum concurrent sessions to prevent DoS + +/** + * Base MCP Server class with programmatic launch() API + * Each platform extends this to provide their own tools manager + */ +export abstract class BaseMCPServer { + protected mcpServer: McpServer; + protected toolsManager?: IMidsceneTools; + protected config: BaseMCPServerConfig; + + constructor(config: BaseMCPServerConfig) { + this.config = config; + this.mcpServer = new McpServer({ + name: config.name, + version: config.version, + description: config.description, + }); + } + + /** + * Platform-specific: create tools manager instance + */ + protected abstract createToolsManager(): IMidsceneTools; + + /** + * Initialize tools manager and attach to MCP server + */ + private async initializeToolsManager(): Promise { + setIsMcp(true); + this.toolsManager = this.createToolsManager(); + + try { + await this.toolsManager.initTools(); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + console.error(`Failed to initialize tools: ${message}`); + console.error('Tools will be initialized on first use'); + } + + this.toolsManager.attachToServer(this.mcpServer); + } + + /** + * Perform cleanup on shutdown + */ + private performCleanup(): void { + console.error(`${this.config.name} closing...`); + this.mcpServer.close(); + this.toolsManager?.closeBrowser?.().catch(console.error); + } + + /** + * Initialize and launch the MCP server with stdio transport + */ + public async launch(): Promise { + await this.initializeToolsManager(); + + const transport = new StdioServerTransport(); + + try { + await this.mcpServer.connect(transport); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + console.error(`Failed to connect MCP stdio transport: ${message}`); + throw new Error(`Failed to initialize MCP stdio transport: ${message}`); + } + + // Setup cleanup handlers + process.stdin.on('close', () => this.performCleanup()); + + // Setup signal handlers for graceful shutdown + const cleanup = () => { + console.error(`${this.config.name} shutting down...`); + this.performCleanup(); + process.exit(0); + }; + + process.once('SIGINT', cleanup); + process.once('SIGTERM', cleanup); + } + + /** + * Launch MCP server with HTTP transport + * Supports stateful sessions for web applications and service integration + */ + public async launchHttp(options: HttpLaunchOptions): Promise { + // Validate port number + if ( + !Number.isInteger(options.port) || + options.port < 1 || + options.port > 65535 + ) { + throw new Error( + `Invalid port number: ${options.port}. Port must be between 1 and 65535.`, + ); + } + + await this.initializeToolsManager(); + + const express = await import('express'); + const app: Application = express.default(); + + // Add JSON body parser with size limit + app.use(express.default.json({ limit: '10mb' })); + + const sessions = new Map(); + + app.all('/mcp', async (req: Request, res: Response) => { + const startTime = Date.now(); + const requestId = randomUUID().substring(0, 8); + + try { + const rawSessionId = req.headers['mcp-session-id']; + const sessionId = Array.isArray(rawSessionId) + ? rawSessionId[0] + : rawSessionId; + let session = sessionId ? sessions.get(sessionId) : undefined; + + if (!session && req.method === 'POST') { + // Check session limit to prevent DoS + if (sessions.size >= MAX_SESSIONS) { + console.error( + `[${new Date().toISOString()}] [${requestId}] Session limit reached: ${sessions.size}/${MAX_SESSIONS}`, + ); + res.status(503).json({ + error: 'Too many active sessions', + message: 'Server is at maximum capacity. Please try again later.', + }); + return; + } + session = await this.createHttpSession(sessions); + console.log( + `[${new Date().toISOString()}] [${requestId}] New session created: ${session.transport.sessionId}`, + ); + } + + if (session) { + session.lastAccessedAt = new Date(); + await session.transport.handleRequest(req, res, req.body); + const duration = Date.now() - startTime; + console.log( + `[${new Date().toISOString()}] [${requestId}] Request completed in ${duration}ms`, + ); + } else { + console.error( + `[${new Date().toISOString()}] [${requestId}] Invalid session or GET without session`, + ); + res + .status(400) + .json({ error: 'Invalid session or GET without session' }); + } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + const duration = Date.now() - startTime; + console.error( + `[${new Date().toISOString()}] [${requestId}] MCP request error after ${duration}ms: ${message}`, + ); + if (!res.headersSent) { + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to process MCP request', + }); + } + } + }); + + const host = options.host || 'localhost'; + + // Create server with error handling + const server = app + .listen(options.port, host, () => { + console.log( + `${this.config.name} HTTP server listening on http://${host}:${options.port}/mcp`, + ); + }) + .on('error', (error: NodeJS.ErrnoException) => { + if (error.code === 'EADDRINUSE') { + console.error( + `ERROR: Port ${options.port} is already in use.\nPlease try a different port: --port=\nExample: --mode=http --port=${options.port + 1}`, + ); + } else if (error.code === 'EACCES') { + console.error( + `ERROR: Permission denied to bind to port ${options.port}.\nPorts below 1024 require root/admin privileges.\nPlease use a port above 1024 or run with elevated privileges.`, + ); + } else { + console.error( + `ERROR: Failed to start HTTP server on ${host}:${options.port}\n` + + `Reason: ${error.message}\n` + + `Code: ${error.code || 'unknown'}`, + ); + } + process.exit(1); + }); + + const cleanupInterval = this.startSessionCleanup(sessions); + this.setupHttpShutdownHandlers(server, sessions, cleanupInterval); + } + + /** + * Create a new HTTP session with transport + */ + private async createHttpSession( + sessions: Map, + ): Promise { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sid: string) => { + sessions.set(sid, { + transport, + createdAt: new Date(), + lastAccessedAt: new Date(), + }); + console.log( + `[${new Date().toISOString()}] Session ${sid} initialized (total: ${sessions.size})`, + ); + }, + }); + + transport.onclose = () => { + if (transport.sessionId) { + sessions.delete(transport.sessionId); + console.log( + `[${new Date().toISOString()}] Session ${transport.sessionId} closed (remaining: ${sessions.size})`, + ); + } + }; + + try { + await this.mcpServer.connect(transport); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + console.error( + `[${new Date().toISOString()}] Failed to connect MCP transport: ${message}`, + ); + // Clean up the failed transport + if (transport.sessionId) { + sessions.delete(transport.sessionId); + } + throw new Error(`Failed to initialize MCP session: ${message}`); + } + + return { + transport, + createdAt: new Date(), + lastAccessedAt: new Date(), + }; + } + + /** + * Start periodic session cleanup for inactive sessions + */ + private startSessionCleanup( + sessions: Map, + ): ReturnType { + return setInterval(() => { + const now = Date.now(); + for (const [sid, session] of sessions) { + if (now - session.lastAccessedAt.getTime() > SESSION_TIMEOUT_MS) { + try { + session.transport.close(); + sessions.delete(sid); + console.log( + `[${new Date().toISOString()}] Session ${sid} cleaned up due to inactivity (remaining: ${sessions.size})`, + ); + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : String(error); + console.error( + `[${new Date().toISOString()}] Failed to close session ${sid} during cleanup: ${message}`, + ); + // Still delete from map to prevent retry loops + sessions.delete(sid); + } + } + } + }, CLEANUP_INTERVAL_MS); + } + + /** + * Setup shutdown handlers for HTTP server + */ + private setupHttpShutdownHandlers( + server: ReturnType, + sessions: Map, + cleanupInterval: ReturnType, + ): void { + const cleanup = () => { + console.error(`${this.config.name} shutting down...`); + clearInterval(cleanupInterval); + + // Close all sessions with error handling + for (const session of sessions.values()) { + try { + session.transport.close(); + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : String(error); + console.error(`Error closing session during shutdown: ${message}`); + } + } + sessions.clear(); + + // Close HTTP server gracefully + try { + server.close(() => { + // Server closed callback - all connections finished + this.performCleanup(); + process.exit(0); + }); + + // Set a timeout in case server.close() hangs + setTimeout(() => { + console.error('Forcefully shutting down after timeout'); + this.performCleanup(); + process.exit(1); + }, 5000); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + console.error(`Error closing HTTP server: ${message}`); + this.performCleanup(); + process.exit(1); + } + }; + + // Use once() to prevent multiple registrations + process.once('SIGINT', cleanup); + process.once('SIGTERM', cleanup); + } + + /** + * Get the underlying MCP server instance + */ + public getServer(): McpServer { + return this.mcpServer; + } + + /** + * Get the tools manager instance + */ + public getToolsManager(): IMidsceneTools | undefined { + return this.toolsManager; + } +} diff --git a/packages/shared/src/mcp/base-tools.ts b/packages/shared/src/mcp/base-tools.ts new file mode 100644 index 000000000..f4eaec465 --- /dev/null +++ b/packages/shared/src/mcp/base-tools.ts @@ -0,0 +1,190 @@ +import { parseBase64 } from '@midscene/shared/img'; +import { getDebug } from '@midscene/shared/logger'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { + generateCommonTools, + generateToolsFromActionSpace, +} from './tool-generator'; +import type { + ActionSpaceItem, + BaseAgent, + BaseDevice, + IMidsceneTools, + ToolDefinition, +} from './types'; + +const debug = getDebug('mcp:base-tools'); + +export abstract class BaseMidsceneTools implements IMidsceneTools { + protected mcpServer?: McpServer; + protected agent?: BaseAgent; + protected toolDefinitions: ToolDefinition[] = []; + + /** + * Ensure agent is initialized and ready for use. + * Must be implemented by subclasses to create platform-specific agent. + * @param initParam Optional initialization parameter (platform-specific, e.g., URL, device ID) + * @returns Promise resolving to initialized agent instance + * @throws Error if agent initialization fails + */ + protected abstract ensureAgent(initParam?: string): Promise; + + /** + * Optional: prepare platform-specific tools (e.g., device connection) + */ + protected preparePlatformTools(): ToolDefinition[] { + return []; + } + + /** + * Must be implemented by subclasses to create a temporary device instance + * This allows getting real actionSpace without connecting to device + */ + protected abstract createTemporaryDevice(): BaseDevice; + + /** + * Initialize all tools by querying actionSpace + * Uses two-layer fallback strategy: + * 1. Try to get actionSpace from connected agent (if available) + * 2. Create temporary device instance to read actionSpace (always succeeds) + */ + public async initTools(): Promise { + this.toolDefinitions = []; + + // 1. Add platform-specific tools first (device connection, etc.) + // These don't require an agent and should always be available + const platformTools = this.preparePlatformTools(); + this.toolDefinitions.push(...platformTools); + + // 2. Try to get agent and its action space (two-layer fallback) + let actionSpace: ActionSpaceItem[]; + try { + // Layer 1: Try to use connected agent + const agent = await this.ensureAgent(); + actionSpace = await agent.getActionSpace(); + debug( + 'Action space from connected agent:', + actionSpace.map((a) => a.name).join(', '), + ); + } catch (error) { + // Layer 2: Create temporary device instance to read actionSpace + // This is expected behavior for bridge mode without URL or unconnected devices + const errorMessage = + error instanceof Error ? error.message : String(error); + if ( + errorMessage.includes('requires a URL') || + errorMessage.includes('web_connect') + ) { + debug( + 'Bridge mode detected - agent will be initialized on first web_connect call', + ); + } else { + debug( + 'Agent not available yet, using temporary device for action space', + ); + } + const tempDevice = this.createTemporaryDevice(); + actionSpace = tempDevice.actionSpace(); + debug( + 'Action space from temporary device:', + actionSpace.map((a) => a.name).join(', '), + ); + + // Destroy temporary instance using optional chaining + await tempDevice.destroy?.(); + } + + // 3. Generate tools from action space (core innovation) + const actionTools = generateToolsFromActionSpace(actionSpace, () => + this.ensureAgent(), + ); + + // 4. Add common tools (screenshot, waitFor) + const commonTools = generateCommonTools(() => this.ensureAgent()); + + this.toolDefinitions.push(...actionTools, ...commonTools); + + debug('Total tools prepared:', this.toolDefinitions.length); + } + + /** + * Attach to MCP server and register all tools + */ + public attachToServer(server: McpServer): void { + this.mcpServer = server; + + if (this.toolDefinitions.length === 0) { + debug('Warning: No tools to register. Tools may be initialized lazily.'); + } + + for (const toolDef of this.toolDefinitions) { + if (toolDef.autoDestroy) { + this.toolWithAutoDestroy( + toolDef.name, + toolDef.description, + toolDef.schema, + toolDef.handler, + ); + } else { + this.mcpServer.tool( + toolDef.name, + toolDef.description, + toolDef.schema, + toolDef.handler, + ); + } + } + + debug('Registered', this.toolDefinitions.length, 'tools'); + } + + /** + * Wrapper for auto-destroy behavior + */ + private toolWithAutoDestroy( + name: string, + description: string, + schema: any, + handler: (...args: any[]) => Promise, + ): void { + if (!this.mcpServer) { + throw new Error('MCP server not attached'); + } + + this.mcpServer.tool(name, description, schema, async (...args: any[]) => { + try { + return await handler(...args); + } finally { + if (!process.env.MIDSCENE_MCP_DISABLE_AGENT_AUTO_DESTROY) { + try { + await this.agent?.destroy?.(); + } catch (error) { + debug('Failed to destroy agent during cleanup:', error); + } + this.agent = undefined; + } + } + }); + } + + /** + * Cleanup method - destroy agent and release resources + */ + public async closeBrowser(): Promise { + await this.agent?.destroy?.(); + } + + /** + * Helper: Convert base64 screenshot to image content array + */ + protected buildScreenshotContent(screenshot: string) { + const { mimeType, body } = parseBase64(screenshot); + return [ + { + type: 'image' as const, + data: body, + mimeType, + }, + ]; + } +} diff --git a/packages/shared/src/mcp/index.ts b/packages/shared/src/mcp/index.ts new file mode 100644 index 000000000..e219d8e44 --- /dev/null +++ b/packages/shared/src/mcp/index.ts @@ -0,0 +1,4 @@ +export * from './base-server'; +export * from './base-tools'; +export * from './tool-generator'; +export * from './types'; diff --git a/packages/shared/src/mcp/tool-generator.ts b/packages/shared/src/mcp/tool-generator.ts new file mode 100644 index 000000000..6ac4b3bf9 --- /dev/null +++ b/packages/shared/src/mcp/tool-generator.ts @@ -0,0 +1,137 @@ +import { parseBase64 } from '@midscene/shared/img'; +import { z } from 'zod'; +import type { ActionSpaceItem, BaseAgent, ToolDefinition } from './types'; + +/** + * Converts DeviceAction from actionSpace into MCP ToolDefinition + * This is the core logic that removes need for hardcoded tool definitions + */ +export function generateToolsFromActionSpace( + actionSpace: ActionSpaceItem[], + getAgent: () => Promise, +): ToolDefinition[] { + return actionSpace.map((action) => { + // Extract the shape from Zod schema if it exists + // For z.object({ locate: ... }), we want to get the shape (the fields inside) + let schema: Record = {}; + if (action.paramSchema) { + const paramSchema = action.paramSchema as z.ZodTypeAny; + // If it's a ZodObject, extract its shape + if ( + '_def' in paramSchema && + paramSchema._def?.typeName === 'ZodObject' && + 'shape' in paramSchema + ) { + schema = (paramSchema as z.ZodObject).shape; + } else { + // Otherwise use it as-is + schema = paramSchema as unknown as Record; + } + } + + return { + name: action.name, + description: action.description || `Execute ${action.name} action`, + schema, + handler: async (args: Record) => { + const agent = await getAgent(); + + // Call the action through agent's aiAction method + // args already contains the unwrapped parameters (e.g., { locate: {...} }) + if (agent.aiAction) { + await agent.aiAction(`Use the action "${action.name}"`, { + ...args, + }); + } + + // Return screenshot after action + const screenshot = await agent.page?.screenshotBase64(); + if (!screenshot) { + return { + content: [ + { + type: 'text', + text: `Action "${action.name}" completed.`, + }, + ], + }; + } + + const { mimeType, body } = parseBase64(screenshot); + + return { + content: [ + { + type: 'text', + text: `Action "${action.name}" completed.`, + }, + { + type: 'image', + data: body, + mimeType, + }, + ], + }; + }, + autoDestroy: true, + }; + }); +} + +/** + * Generate common tools (screenshot, waitFor) + * SIMPLIFIED: Only keep essential helper tools, removed assert + */ +export function generateCommonTools( + getAgent: () => Promise, +): ToolDefinition[] { + return [ + { + name: 'take_screenshot', + description: 'Capture screenshot of current page/screen', + schema: {}, + handler: async () => { + const agent = await getAgent(); + const screenshot = await agent.page?.screenshotBase64(); + if (!screenshot) { + return { + content: [{ type: 'text', text: 'Screenshot not available' }], + isError: true, + }; + } + const { mimeType, body } = parseBase64(screenshot); + return { + content: [{ type: 'image', data: body, mimeType }], + }; + }, + autoDestroy: true, + }, + { + name: 'wait_for', + description: 'Wait until condition becomes true', + schema: { + assertion: z.string().describe('Condition to wait for'), + timeoutMs: z.number().optional().default(15000), + checkIntervalMs: z.number().optional().default(3000), + }, + handler: async (args) => { + const agent = await getAgent(); + const { assertion, timeoutMs, checkIntervalMs } = args as { + assertion: string; + timeoutMs?: number; + checkIntervalMs?: number; + }; + + if (agent.aiWaitFor) { + await agent.aiWaitFor(assertion, { timeoutMs, checkIntervalMs }); + } + + return { + content: [{ type: 'text', text: `Condition met: "${assertion}"` }], + isError: false, + }; + }, + autoDestroy: true, + }, + ]; +} diff --git a/packages/shared/src/mcp/types.ts b/packages/shared/src/mcp/types.ts new file mode 100644 index 000000000..5c8d91539 --- /dev/null +++ b/packages/shared/src/mcp/types.ts @@ -0,0 +1,106 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { z } from 'zod'; + +// Avoid circular dependency: don't import from @midscene/core +// Instead, use generic types that will be provided by implementation + +/** + * Default timeout constants for app loading verification + */ +export const defaultAppLoadingTimeoutMs = 10000; +export const defaultAppLoadingCheckIntervalMs = 2000; + +/** + * Content item types for tool results (MCP compatible) + */ +export type ToolResultContent = + | { type: 'text'; text: string } + | { type: 'image'; data: string; mimeType: string } + | { type: 'audio'; data: string; mimeType: string } + | { + type: 'resource'; + resource: + | { text: string; uri: string; mimeType?: string } + | { uri: string; blob: string; mimeType?: string }; + }; + +/** + * Result type for tool execution (MCP compatible) + */ +export interface ToolResult { + [x: string]: unknown; + content: ToolResultContent[]; + isError?: boolean; + _meta?: Record; +} + +/** + * Tool handler function type + * Takes parsed arguments and returns a tool result + */ +export type ToolHandler> = ( + args: T, +) => Promise; + +/** + * Tool schema type using Zod + */ +export type ToolSchema = Record; + +/** + * Tool definition for MCP server + */ +export interface ToolDefinition> { + name: string; + description: string; + schema: ToolSchema; + handler: ToolHandler; + autoDestroy?: boolean; +} + +/** + * Action space item definition + */ +export interface ActionSpaceItem { + name: string; + description?: string; + args?: Record; + [key: string]: unknown; +} + +/** + * Base agent interface + * Represents a platform-specific agent (Android, iOS, Web) + */ +export interface BaseAgent { + getActionSpace(): Promise; + destroy?(): Promise; + page?: { + screenshotBase64(): Promise; + }; + aiAction?: ( + description: string, + params: Record, + ) => Promise; + aiWaitFor?: ( + assertion: string, + options: Record, + ) => Promise; +} + +/** + * Base device interface for temporary device instances + */ +export interface BaseDevice { + actionSpace(): ActionSpaceItem[]; + destroy?(): Promise; +} + +/** + * Interface for platform-specific MCP tools manager + */ +export interface IMidsceneTools { + attachToServer(server: McpServer): void; + initTools(): Promise; + closeBrowser?(): Promise; +} diff --git a/packages/visualizer/package.json b/packages/visualizer/package.json index a3c05142b..a01a10e9c 100644 --- a/packages/visualizer/package.json +++ b/packages/visualizer/package.json @@ -30,7 +30,7 @@ "@rsbuild/plugin-node-polyfill": "1.4.2", "@rsbuild/plugin-react": "^1.4.1", "@rsbuild/plugin-svgr": "^1.2.2", - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", "@types/chrome": "0.0.279", "@types/node": "^18.0.0", "@types/react": "^18.3.1", diff --git a/packages/web-bridge-mcp/README.md b/packages/web-bridge-mcp/README.md new file mode 100644 index 000000000..64c48312a --- /dev/null +++ b/packages/web-bridge-mcp/README.md @@ -0,0 +1,3 @@ +# Midscene MCP + +docs: https://midscenejs.com/mcp.html diff --git a/packages/web-bridge-mcp/package.json b/packages/web-bridge-mcp/package.json new file mode 100644 index 000000000..62779cc01 --- /dev/null +++ b/packages/web-bridge-mcp/package.json @@ -0,0 +1,50 @@ +{ + "name": "@midscene/web-bridge-mcp", + "version": "1.0.0", + "description": "Midscene MCP Server for Web automation (Bridge mode)", + "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/core": "workspace:*", + "@midscene/report": "workspace:*", + "@midscene/shared": "workspace:*", + "@midscene/web": "workspace:*", + "@modelcontextprotocol/inspector": "^0.16.3", + "@modelcontextprotocol/sdk": "1.10.2", + "@rslib/core": "^0.18.3", + "@rspack/core": "1.6.6", + "@types/node": "^18.0.0", + "dotenv": "^16.4.5", + "puppeteer-core": "24.2.0", + "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" +} diff --git a/packages/web-bridge-mcp/rslib.config.ts b/packages/web-bridge-mcp/rslib.config.ts new file mode 100644 index 000000000..4c7736b56 --- /dev/null +++ b/packages/web-bridge-mcp/rslib.config.ts @@ -0,0 +1,56 @@ +import { defineConfig } from '@rslib/core'; +import { rspack } from '@rspack/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', + ], + }, + tools: { + rspack: { + plugins: [ + new rspack.BannerPlugin({ + banner: '#!/usr/bin/env node', + raw: true, + test: /^index\.js$/, + }), + ], + optimization: { + minimize: false, + }, + }, + }, + lib: [ + { + format: 'cjs', + syntax: 'es2021', + output: { + distPath: { + root: 'dist', + }, + }, + }, + ], +}); diff --git a/packages/web-bridge-mcp/src/index.ts b/packages/web-bridge-mcp/src/index.ts new file mode 100644 index 000000000..bf2bf670d --- /dev/null +++ b/packages/web-bridge-mcp/src/index.ts @@ -0,0 +1,19 @@ +import { parseArgs } from 'node:util'; +import { type CLIArgs, CLI_ARGS_CONFIG } from '@midscene/shared/mcp'; +import { WebMCPServer } from './server.js'; + +const { values } = parseArgs({ options: CLI_ARGS_CONFIG }); +const args = values as CLIArgs; + +const server = new WebMCPServer(); + +if (args.mode === 'http') { + server + .launchHttp({ + port: Number.parseInt(args.port || '3000', 10), + host: args.host || 'localhost', + }) + .catch(console.error); +} else { + server.launch().catch(console.error); +} diff --git a/packages/web-bridge-mcp/src/server.ts b/packages/web-bridge-mcp/src/server.ts new file mode 100644 index 000000000..ae8651ca6 --- /dev/null +++ b/packages/web-bridge-mcp/src/server.ts @@ -0,0 +1,24 @@ +import { BaseMCPServer } from '@midscene/shared/mcp'; +import { WebMidsceneTools } from './web-tools.js'; + +declare const __VERSION__: string; + +/** + * Web MCP Server class + * Usage: + * const server = new WebMCPServer(); + * await server.launch(); + */ +export class WebMCPServer extends BaseMCPServer { + constructor() { + super({ + name: '@midscene/web-bridge-mcp', + version: __VERSION__, + description: 'Midscene MCP Server for Web automation (Bridge mode)', + }); + } + + protected createToolsManager(): WebMidsceneTools { + return new WebMidsceneTools(); + } +} diff --git a/packages/mcp/src/utils.ts b/packages/web-bridge-mcp/src/utils.ts similarity index 100% rename from packages/mcp/src/utils.ts rename to packages/web-bridge-mcp/src/utils.ts diff --git a/packages/web-bridge-mcp/src/web-tools.ts b/packages/web-bridge-mcp/src/web-tools.ts new file mode 100644 index 000000000..23e273714 --- /dev/null +++ b/packages/web-bridge-mcp/src/web-tools.ts @@ -0,0 +1,98 @@ +import { z } from '@midscene/core'; +import { + type BaseAgent, + BaseMidsceneTools, + type ToolDefinition, +} from '@midscene/shared/mcp'; +import { AgentOverChromeBridge } from '@midscene/web/bridge-mode'; + +export class WebMidsceneTools extends BaseMidsceneTools { + protected createTemporaryDevice() { + // Use require to avoid type incompatibility with DeviceAction vs ActionSpaceItem + // StaticPage.actionSpace() returns DeviceAction[] which is compatible at runtime + const { StaticPage } = require('@midscene/web/static'); + return new StaticPage({ + screenshotBase64: '', + size: { width: 1920, height: 1080 }, + }); + } + + protected async ensureAgent(openNewTabWithUrl?: string): Promise { + // Re-init if URL provided + if (this.agent && openNewTabWithUrl) { + try { + await this.agent?.destroy?.(); + } catch (error) { + console.debug('Failed to destroy agent during re-init:', error); + } + this.agent = undefined; + } + + if (this.agent) return this.agent; + + // Bridge mode requires a URL to connect to browser + if (!openNewTabWithUrl) { + throw new Error( + 'Bridge mode requires a URL. Use web_connect tool to connect to a page first.', + ); + } + + this.agent = (await this.initBridgeModeAgent( + openNewTabWithUrl, + )) as unknown as BaseAgent; + + return this.agent; + } + + private async initBridgeModeAgent( + url?: string, + ): Promise { + const agent = new AgentOverChromeBridge({ closeConflictServer: true }); + + if (!url) { + await agent.connectCurrentTab(); + } else { + await agent.connectNewTabWithUrl(url); + } + + return agent; + } + + protected preparePlatformTools(): ToolDefinition[] { + return [ + { + name: 'web_connect', + description: 'Connect to web page by opening new tab with URL', + schema: { + url: z.string().url().describe('URL to connect to'), + }, + handler: async (args) => { + const { url } = args as { url: string }; + const agent = await this.ensureAgent(url); + const screenshot = await agent.page?.screenshotBase64(); + if (!screenshot) { + return { + content: [ + { + type: 'text', + text: `Connected to: ${url}`, + }, + ], + }; + } + + return { + content: [ + { + type: 'text', + text: `Connected to: ${url}`, + }, + ...this.buildScreenshotContent(screenshot), + ], + }; + }, + autoDestroy: false, + }, + ]; + } +} diff --git a/packages/web-bridge-mcp/tests/http-server.test.ts b/packages/web-bridge-mcp/tests/http-server.test.ts new file mode 100644 index 000000000..2182f6df6 --- /dev/null +++ b/packages/web-bridge-mcp/tests/http-server.test.ts @@ -0,0 +1,124 @@ +import type { Server } from 'node:http'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { WebMCPServer } from '../src/server.js'; + +describe('WebMCPServer HTTP mode', () => { + let server: WebMCPServer; + const httpServer: Server | null = null; + const testPort = 13579; // Use a non-standard port for testing + const testHost = '127.0.0.1'; // Use IPv4 explicitly to avoid IPv6 issues in CI + + beforeAll(async () => { + server = new WebMCPServer(); + }); + + afterAll(async () => { + // Close the HTTP server + if (httpServer) { + await new Promise((resolve) => { + httpServer!.close(() => resolve()); + }); + } + }); + + it('should start HTTP server successfully and respond to requests', async () => { + // Mock process.exit to prevent test exit + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit called'); + }) as any); + + try { + // Start server in background + const serverPromise = server.launchHttp({ + port: testPort, + host: testHost, + }); + + // Give server time to start + await new Promise((resolve) => setTimeout(resolve, 1500)); + + // Simply verify the server is listening by trying to connect + const response = await fetch(`http://${testHost}:${testPort}/mcp`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'initialize', + params: { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { + name: 'test-client', + version: '1.0.0', + }, + }, + id: 1, + }), + }); + + // Server should respond (even if it's SSE format, it should return 200) + expect(response.status).toBe(200); + + // Verify content-type header indicates SSE or JSON + const contentType = response.headers.get('content-type'); + expect(contentType).toBeTruthy(); + expect( + contentType?.includes('text/event-stream') || + contentType?.includes('application/json'), + ).toBe(true); + + console.log('✓ Server started and responding successfully'); + } finally { + exitSpy.mockRestore(); + } + }, 15000); + + it('should reject invalid port numbers', async () => { + const invalidServer = new WebMCPServer(); + + await expect( + invalidServer.launchHttp({ + port: -1, + host: testHost, + }), + ).rejects.toThrow(/Invalid port number/); + + await expect( + invalidServer.launchHttp({ + port: 99999, + host: testHost, + }), + ).rejects.toThrow(/Invalid port number/); + }); + + it('should handle port already in use error gracefully', async () => { + // Mock process.exit to prevent test exit + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { + // Just throw an error instead of actually exiting + }) as any); + + try { + // First server is already running on testPort from previous test + const conflictServer = new WebMCPServer(); + + // This should fail because the port is already in use + const launchPromise = conflictServer.launchHttp({ + port: testPort, + host: testHost, + }); + + // Wait for the error to be detected + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Verify that process.exit was called with error code + expect(exitSpy).toHaveBeenCalledWith(1); + + console.log('✓ Port conflict handled correctly'); + } finally { + exitSpy.mockRestore(); + } + }, 5000); +}); diff --git a/packages/web-bridge-mcp/tests/tsconfig.json b/packages/web-bridge-mcp/tests/tsconfig.json new file mode 100644 index 000000000..eba9379e3 --- /dev/null +++ b/packages/web-bridge-mcp/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "include": [".", "../src/**/*", "../vitest.setup.ts"], + "compilerOptions": { + "types": ["vitest/globals", "node"] + } +} diff --git a/packages/mcp/tests/index.test.ts b/packages/web-bridge-mcp/tests/utils.test.ts similarity index 97% rename from packages/mcp/tests/index.test.ts rename to packages/web-bridge-mcp/tests/utils.test.ts index cbbd404d6..9d5080380 100644 --- a/packages/mcp/tests/index.test.ts +++ b/packages/web-bridge-mcp/tests/utils.test.ts @@ -1,6 +1,5 @@ import { existsSync } from 'node:fs'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; -import { tools } from '../src/tools'; import { deepMerge, getChromePathFromEnv, @@ -175,9 +174,3 @@ describe('Utils Module', () => { }); }); }); - -describe('Tools Module', () => { - test('should have all expected tools defined', () => { - expect(tools).toMatchSnapshot(); - }); -}); diff --git a/packages/web-bridge-mcp/tsconfig.json b/packages/web-bridge-mcp/tsconfig.json new file mode 100644 index 000000000..83e2763d7 --- /dev/null +++ b/packages/web-bridge-mcp/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../shared/tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2021"], + "noEmit": true, + "useDefineForClassFields": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/packages/web-bridge-mcp/vitest.config.ts b/packages/web-bridge-mcp/vitest.config.ts new file mode 100644 index 000000000..7c747ac04 --- /dev/null +++ b/packages/web-bridge-mcp/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; +import { version } from './package.json'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + define: { + __VERSION__: JSON.stringify(version), + }, + ssr: { + external: ['@silvia-odwyer/photon'], + }, +}); diff --git a/packages/web-integration/package.json b/packages/web-integration/package.json index af69a0d12..a86efc475 100644 --- a/packages/web-integration/package.json +++ b/packages/web-integration/package.json @@ -119,7 +119,7 @@ }, "devDependencies": { "@playwright/test": "^1.44.1", - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", "@types/chrome": "0.0.279", "@types/cors": "^2.8.17", "@types/http-server": "^0.12.4", diff --git a/packages/webdriver/package.json b/packages/webdriver/package.json index 2b74fd18b..e9f77c411 100644 --- a/packages/webdriver/package.json +++ b/packages/webdriver/package.json @@ -18,7 +18,7 @@ "node-fetch": "^3.3.2" }, "devDependencies": { - "@rslib/core": "^0.18.2", + "@rslib/core": "^0.18.3", "@types/node": "^18.0.0", "typescript": "^5.8.3", "vitest": "3.0.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6ae12191..b00cc98a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,10 +51,10 @@ importers: specifier: ^0.8.0 version: 0.8.0 nx: - specifier: 21.1.2 - version: 21.1.2 + specifier: 22.1.3 + version: 22.1.3 prettier: - specifier: ^3.7.4 + specifier: ^3.6.2 version: 3.7.4 pretty-quick: specifier: 3.1.3 @@ -131,7 +131,7 @@ importers: version: 1.2.2(@rsbuild/core@1.6.9)(typescript@5.8.3) '@rsbuild/plugin-type-check': specifier: 1.2.4 - version: 1.2.4(@rsbuild/core@1.6.9)(@rspack/core@1.6.5)(typescript@5.8.3) + version: 1.2.4(@rsbuild/core@1.6.9)(@rspack/core@1.6.6)(typescript@5.8.3) '@types/react': specifier: ^18.3.1 version: 18.3.23 @@ -225,7 +225,7 @@ importers: version: 1.2.2(@rsbuild/core@1.6.9)(typescript@5.8.3) '@rsbuild/plugin-type-check': specifier: 1.2.4 - version: 1.2.4(@rsbuild/core@1.6.9)(@rspack/core@1.6.5)(typescript@5.8.3) + version: 1.2.4(@rsbuild/core@1.6.9)(@rspack/core@1.6.6)(typescript@5.8.3) '@tailwindcss/postcss': specifier: 4.1.11 version: 4.1.11 @@ -310,7 +310,7 @@ importers: version: 1.2.2(@rsbuild/core@1.6.9)(typescript@5.8.3) '@rsbuild/plugin-type-check': specifier: 1.2.4 - version: 1.2.4(@rsbuild/core@1.6.9)(@rspack/core@1.6.5)(typescript@5.8.3) + version: 1.2.4(@rsbuild/core@1.6.9)(@rspack/core@1.6.6)(typescript@5.8.3) '@types/react': specifier: ^18.3.1 version: 18.3.23 @@ -365,7 +365,7 @@ importers: version: 1.4.1(@rsbuild/core@1.6.9) '@rsbuild/plugin-type-check': specifier: 1.2.4 - version: 1.2.4(@rsbuild/core@1.6.9)(@rspack/core@1.6.5)(typescript@5.8.3) + version: 1.2.4(@rsbuild/core@1.6.9)(@rspack/core@1.6.6)(typescript@5.8.3) '@types/react': specifier: ^18.3.1 version: 18.3.23 @@ -407,7 +407,7 @@ importers: version: 1.4.1(@rsbuild/core@1.6.9) '@rsbuild/plugin-type-check': specifier: 1.2.4 - version: 1.2.4(@rsbuild/core@1.6.9)(@rspack/core@1.6.5)(typescript@5.8.3) + version: 1.2.4(@rsbuild/core@1.6.9)(@rspack/core@1.6.6)(typescript@5.8.3) '@types/chrome': specifier: 0.0.279 version: 0.0.279 @@ -441,7 +441,7 @@ importers: version: 1.2.2(@rsbuild/core@1.6.9)(typescript@5.8.3) '@rsdoctor/rspack-plugin': specifier: 1.0.2 - version: 1.0.2(@rsbuild/core@1.6.9)(@rspack/core@1.6.5(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) + version: 1.0.2(@rsbuild/core@1.6.9)(@rspack/core@1.6.6(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) '@types/react': specifier: ^18.3.1 version: 18.3.23 @@ -511,8 +511,8 @@ importers: specifier: workspace:* version: link:../playground '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) '@types/node': specifier: ^18.0.0 version: 18.19.62 @@ -532,6 +532,58 @@ importers: specifier: 3.24.3 version: 3.24.3 + packages/android-mcp: + dependencies: + '@silvia-odwyer/photon': + specifier: 0.3.3 + version: 0.3.3 + '@silvia-odwyer/photon-node': + specifier: 0.3.3 + version: 0.3.3 + bufferutil: + specifier: 4.0.9 + version: 4.0.9 + sharp: + specifier: ^0.34.3 + version: 0.34.3 + utf-8-validate: + specifier: 6.0.5 + version: 6.0.5 + devDependencies: + '@midscene/android': + specifier: workspace:* + version: link:../android + '@midscene/core': + specifier: workspace:* + version: link:../core + '@midscene/shared': + specifier: workspace:* + version: link:../shared + '@modelcontextprotocol/inspector': + specifier: ^0.16.3 + version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) + '@modelcontextprotocol/sdk': + specifier: 1.10.2 + version: 1.10.2 + '@rslib/core': + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3) + '@rspack/core': + specifier: 1.6.6 + version: 1.6.6(@swc/helpers@0.5.17) + '@types/node': + specifier: ^18.0.0 + version: 18.19.118 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vitest: + specifier: 3.0.5 + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1) + packages/android-playground: dependencies: '@inquirer/prompts': @@ -578,8 +630,8 @@ importers: version: 4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) devDependencies: '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) '@types/cors': specifier: ^2.8.17 version: 2.8.19 @@ -621,8 +673,8 @@ importers: version: 24.2.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) devDependencies: '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) '@types/js-yaml': specifier: 4.0.9 version: 4.0.9 @@ -709,8 +761,8 @@ importers: version: 3.24.3 devDependencies: '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) '@types/js-yaml': specifier: 4.0.9 version: 4.0.9 @@ -786,8 +838,8 @@ importers: specifier: workspace:* version: link:../playground '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3) + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3) '@types/node': specifier: ^18.0.0 version: 18.19.118 @@ -802,28 +854,12 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1) zod: specifier: 3.24.3 version: 3.24.3 - packages/ios-playground: - dependencies: - '@midscene/ios': - specifier: workspace:* - version: link:../ios - devDependencies: - '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3) - '@types/node': - specifier: ^18.0.0 - version: 18.19.118 - typescript: - specifier: ^5.8.3 - version: 5.8.3 - - packages/mcp: + packages/ios-mcp: dependencies: '@silvia-odwyer/photon': specifier: 0.3.3 @@ -840,6 +876,58 @@ importers: utf-8-validate: specifier: 6.0.5 version: 6.0.5 + devDependencies: + '@midscene/core': + specifier: workspace:* + version: link:../core + '@midscene/ios': + specifier: workspace:* + version: link:../ios + '@midscene/shared': + specifier: workspace:* + version: link:../shared + '@modelcontextprotocol/inspector': + specifier: ^0.16.3 + version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) + '@modelcontextprotocol/sdk': + specifier: 1.10.2 + version: 1.10.2 + '@rslib/core': + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3) + '@rspack/core': + specifier: 1.6.6 + version: 1.6.6(@swc/helpers@0.5.17) + '@types/node': + specifier: ^18.0.0 + version: 18.19.118 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vitest: + specifier: 3.0.5 + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1) + + packages/ios-playground: + dependencies: + '@midscene/ios': + specifier: workspace:* + version: link:../ios + devDependencies: + '@rslib/core': + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3) + '@types/node': + specifier: ^18.0.0 + version: 18.19.118 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + + packages/mcp: devDependencies: '@midscene/android': specifier: workspace:* @@ -858,31 +946,19 @@ importers: version: link:../web-integration '@modelcontextprotocol/inspector': specifier: ^0.16.3 - version: 0.16.3(@types/node@18.19.62)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) + version: 0.16.3(@types/node@18.19.62)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(typescript@5.8.3) '@modelcontextprotocol/sdk': specifier: 1.10.2 version: 1.10.2 '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) '@types/node': specifier: ^18.0.0 version: 18.19.62 - dotenv: - specifier: ^16.4.5 - version: 16.4.5 - puppeteer-core: - specifier: 24.2.0 - version: 24.2.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) typescript: specifier: ^5.8.3 version: 5.8.3 - vitest: - specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1) - zod: - specifier: 3.24.3 - version: 3.24.3 packages/playground: dependencies: @@ -909,8 +985,8 @@ importers: version: 11.1.0 devDependencies: '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3) + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3) '@types/cors': specifier: ^2.8.17 version: 2.8.19 @@ -928,7 +1004,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1) packages/recorder: dependencies: @@ -950,10 +1026,10 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: ^1.4.1 - version: 1.4.1(@rsbuild/core@1.6.9) + version: 1.4.1(@rsbuild/core@1.6.12) '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@24.10.1))(typescript@5.8.3) + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@24.10.1))(typescript@5.8.3) '@types/react': specifier: ^18.3.1 version: 18.3.23 @@ -975,6 +1051,9 @@ importers: debug: specifier: 4.4.0 version: 4.4.0 + express: + specifier: ^4.21.2 + version: 4.21.2 jimp: specifier: 0.22.12 version: 0.22.12(debug@4.4.0) @@ -988,12 +1067,18 @@ importers: specifier: 11.1.0 version: 11.1.0 devDependencies: + '@modelcontextprotocol/sdk': + specifier: 1.10.2 + version: 1.10.2 '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) '@types/debug': specifier: 4.1.12 version: 4.1.12 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 '@types/node': specifier: ^18.0.0 version: 18.19.62 @@ -1005,7 +1090,7 @@ importers: version: 16.4.5 openai: specifier: 6.3.0 - version: 6.3.0(ws@8.18.3)(zod@3.25.76) + version: 6.3.0(ws@8.18.3)(zod@3.24.3) rimraf: specifier: ~3.0.2 version: 3.0.2 @@ -1015,6 +1100,9 @@ importers: vitest: specifier: 3.0.5 version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1) + zod: + specifier: 3.24.3 + version: 3.24.3 packages/visualizer: dependencies: @@ -1048,19 +1136,19 @@ importers: version: 7.4.2(@pixi/core@7.4.3) '@rsbuild/plugin-less': specifier: ^1.5.0 - version: 1.5.0(@rsbuild/core@1.6.9) + version: 1.5.0(@rsbuild/core@1.6.12) '@rsbuild/plugin-node-polyfill': specifier: 1.4.2 - version: 1.4.2(@rsbuild/core@1.6.9) + version: 1.4.2(@rsbuild/core@1.6.12) '@rsbuild/plugin-react': specifier: ^1.4.1 - version: 1.4.1(@rsbuild/core@1.6.9) + version: 1.4.1(@rsbuild/core@1.6.12) '@rsbuild/plugin-svgr': specifier: ^1.2.2 - version: 1.2.2(@rsbuild/core@1.6.9)(typescript@5.8.3) + version: 1.2.2(@rsbuild/core@1.6.12)(typescript@5.8.3) '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) '@types/chrome': specifier: 0.0.279 version: 0.0.279 @@ -1110,6 +1198,64 @@ importers: specifier: 4.5.2 version: 4.5.2(@types/react@18.3.23)(immer@10.1.1)(react@18.3.1) + packages/web-bridge-mcp: + dependencies: + '@silvia-odwyer/photon': + specifier: 0.3.3 + version: 0.3.3 + '@silvia-odwyer/photon-node': + specifier: 0.3.3 + version: 0.3.3 + bufferutil: + specifier: 4.0.9 + version: 4.0.9 + sharp: + specifier: ^0.34.3 + version: 0.34.3 + utf-8-validate: + specifier: 6.0.5 + version: 6.0.5 + devDependencies: + '@midscene/core': + specifier: workspace:* + version: link:../core + '@midscene/report': + specifier: workspace:* + version: link:../../apps/report + '@midscene/shared': + specifier: workspace:* + version: link:../shared + '@midscene/web': + specifier: workspace:* + version: link:../web-integration + '@modelcontextprotocol/inspector': + specifier: ^0.16.3 + version: 0.16.3(@types/node@18.19.118)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5) + '@modelcontextprotocol/sdk': + specifier: 1.10.2 + version: 1.10.2 + '@rslib/core': + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3) + '@rspack/core': + specifier: 1.6.6 + version: 1.6.6(@swc/helpers@0.5.17) + '@types/node': + specifier: ^18.0.0 + version: 18.19.118 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + puppeteer-core: + specifier: 24.2.0 + version: 24.2.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vitest: + specifier: 3.0.5 + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1) + packages/web-integration: dependencies: '@midscene/core': @@ -1144,8 +1290,8 @@ importers: specifier: ^1.44.1 version: 1.44.1 '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) '@types/chrome': specifier: 0.0.279 version: 0.0.279 @@ -1199,8 +1345,8 @@ importers: version: 3.3.2 devDependencies: '@rslib/core': - specifier: ^0.18.2 - version: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3) + specifier: ^0.18.3 + version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3) '@types/node': specifier: ^18.0.0 version: 18.19.118 @@ -1209,7 +1355,7 @@ importers: version: 5.8.3 vitest: specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1) + version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1) packages: @@ -2435,9 +2581,17 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/diff-sequences@30.0.1': + resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jimp/bmp@0.22.12': resolution: {integrity: sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==} @@ -2617,9 +2771,6 @@ packages: '@jridgewell/source-map@0.3.11': resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - '@jridgewell/sourcemap-codec@1.5.4': - resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -2718,21 +2869,39 @@ packages: '@module-federation/error-codes@0.21.4': resolution: {integrity: sha512-ClpL5MereWNXh+EgDjz7w4RrC1JlisQTvXDa1gLxpviHafzNDfdViVmuhi9xXVuj+EYo8KU70Y999KHhk9424Q==} + '@module-federation/error-codes@0.21.6': + resolution: {integrity: sha512-MLJUCQ05KnoVl8xd6xs9a5g2/8U+eWmVxg7xiBMeR0+7OjdWUbHwcwgVFatRIwSZvFgKHfWEiI7wsU1q1XbTRQ==} + '@module-federation/runtime-core@0.21.4': resolution: {integrity: sha512-SGpmoOLGNxZofpTOk6Lxb2ewaoz5wMi93AFYuuJB04HTVcngEK+baNeUZ2D/xewrqNIJoMY6f5maUjVfIIBPUA==} + '@module-federation/runtime-core@0.21.6': + resolution: {integrity: sha512-5Hd1Y5qp5lU/aTiK66lidMlM/4ji2gr3EXAtJdreJzkY+bKcI5+21GRcliZ4RAkICmvdxQU5PHPL71XmNc7Lsw==} + '@module-federation/runtime-tools@0.21.4': resolution: {integrity: sha512-RzFKaL0DIjSmkn76KZRfzfB6dD07cvID84950jlNQgdyoQFUGkqD80L6rIpVCJTY/R7LzR3aQjHnoqmq4JPo3w==} + '@module-federation/runtime-tools@0.21.6': + resolution: {integrity: sha512-fnP+ZOZTFeBGiTAnxve+axGmiYn2D60h86nUISXjXClK3LUY1krUfPgf6MaD4YDJ4i51OGXZWPekeMe16pkd8Q==} + '@module-federation/runtime@0.21.4': resolution: {integrity: sha512-wgvGqryurVEvkicufJmTG0ZehynCeNLklv8kIk5BLIsWYSddZAE+xe4xov1kgH5fIJQAoQNkRauFFjVNlHoAkA==} + '@module-federation/runtime@0.21.6': + resolution: {integrity: sha512-+caXwaQqwTNh+CQqyb4mZmXq7iEemRDrTZQGD+zyeH454JAYnJ3s/3oDFizdH6245pk+NiqDyOOkHzzFQorKhQ==} + '@module-federation/sdk@0.21.4': resolution: {integrity: sha512-tzvhOh/oAfX++6zCDDxuvioHY4Jurf8vcfoCbKFxusjmyKr32GPbwFDazUP+OPhYCc3dvaa9oWU6X/qpUBLfJw==} + '@module-federation/sdk@0.21.6': + resolution: {integrity: sha512-x6hARETb8iqHVhEsQBysuWpznNZViUh84qV2yE7AD+g7uIzHKiYdoWqj10posbo5XKf/147qgWDzKZoKoEP2dw==} + '@module-federation/webpack-bundler-runtime@0.21.4': resolution: {integrity: sha512-dusmR3uPnQh9u9ChQo3M+GLOuGFthfvnh7WitF/a1eoeTfRmXqnMFsXtZCUK+f/uXf+64874Zj/bhAgbBcVHZA==} + '@module-federation/webpack-bundler-runtime@0.21.6': + resolution: {integrity: sha512-7zIp3LrcWbhGuFDTUMLJ2FJvcwjlddqhWGxi/MW3ur1a+HaO8v5tF2nl+vElKmbG1DFLU/52l3PElVcWf/YcsQ==} + '@napi-rs/wasm-runtime@0.2.4': resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} @@ -2751,53 +2920,53 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nx/nx-darwin-arm64@21.1.2': - resolution: {integrity: sha512-9dO32jd+h7SrvQafJph6b7Bsmp2IotTE0w7dAGb4MGBQni3JWCXaxlMMpWUZXWW1pM5uIkFJO5AASW4UOI7w2w==} + '@nx/nx-darwin-arm64@22.1.3': + resolution: {integrity: sha512-4D/jXGsr3jcQ0vBo8aXXZMdfmC3n4OsZ1zjFaOXlF62Ujug+RqI/IvKxycT9r7Lr09PmW2OqBC01NfIWKoBLhg==} cpu: [arm64] os: [darwin] - '@nx/nx-darwin-x64@21.1.2': - resolution: {integrity: sha512-5sf+4PRVg9pDVgD53NE1hoPz4lC8Ni34UovQsOrZgDvwU5mqPbIhTzVYRDH86i/086AcCvjT5tEt7rEcuRwlKw==} + '@nx/nx-darwin-x64@22.1.3': + resolution: {integrity: sha512-XmdccOBp1Lx9DXUzYDX65mkFqFvXaxUKm1d63bfA43vxIYUpR59SASB81KRQ/Q4dgvvU27C0EJuxSJbXsSkSYw==} cpu: [x64] os: [darwin] - '@nx/nx-freebsd-x64@21.1.2': - resolution: {integrity: sha512-E5HR44fimXlQuAgn/tP9esmvxbzt/92AIl0PBT6L3Juh/xYiXKWhda63H4+UNT8AcLRxVXwfZrGPuGCDs+7y/Q==} + '@nx/nx-freebsd-x64@22.1.3': + resolution: {integrity: sha512-O+o4mqPwhKxfdsri4KxDbXbjwIwr04GfTSfA0TwgXs6hFf68qmc45FAmPGrPSvxIJg9+mUVDeFirdS8GcUE0jQ==} cpu: [x64] os: [freebsd] - '@nx/nx-linux-arm-gnueabihf@21.1.2': - resolution: {integrity: sha512-V4n6DE+r12gwJHFjZs+e2GmWYZdhpgA2DYWbsYWRYb1XQCNUg4vPzt+YFzWZ+K2o91k93EBnlLfrag7CqxUslw==} + '@nx/nx-linux-arm-gnueabihf@22.1.3': + resolution: {integrity: sha512-ZIPDgzLq8qmvrZ3Bp+bWXam5uKwahjcChBNtORVtrHQfm4mxov2RMUMKTg2ZsVAWVP64zK+gmzG5LuoZjPMm4Q==} cpu: [arm] os: [linux] - '@nx/nx-linux-arm64-gnu@21.1.2': - resolution: {integrity: sha512-NFhsp27O+mS3r7PWLmJgyZy42WQ72c2pTQSpYfhaBbZPTI5DqBHdANa0sEPmV+ON24qkl5CZKvsmhzjsNmyW6A==} + '@nx/nx-linux-arm64-gnu@22.1.3': + resolution: {integrity: sha512-wgpPaTpQKl+cCkSuE5zamTVrg14mRvT+bLAeN/yHSUgMztvGxwl3Ll+K9DgEcktBo1PLECTWNkVaW8IAsJm4Rg==} cpu: [arm64] os: [linux] - '@nx/nx-linux-arm64-musl@21.1.2': - resolution: {integrity: sha512-BgS9npARwcnw+hoaRsbas6vdBAJRBAj5qSeL57LO8Dva+e/6PYqoNyVJ0BgJ98xPXDpzM/NnpeRsndQGpLyhDw==} + '@nx/nx-linux-arm64-musl@22.1.3': + resolution: {integrity: sha512-o9XmQehSPR2y0RD4evD+Ob3lNFuwsFOL5upVJqZ3rcE6GkJIFPg8SwEP5FaRIS5MwS04fxnek20NZ18BHjjV/g==} cpu: [arm64] os: [linux] - '@nx/nx-linux-x64-gnu@21.1.2': - resolution: {integrity: sha512-tjBINbymQgxnIlNK/m6B0P5eiGRSHSYPNkFdh3+sra80AP/ymHGLRxxZy702Ga2xg8RVr9zEvuXYHI+QBa1YmA==} + '@nx/nx-linux-x64-gnu@22.1.3': + resolution: {integrity: sha512-ekcinyDNTa2huVe02T2SFMR8oArohozRbMGO19zftbObXXI4dLdoAuLNb3vK9Pe4vYOpkhfxBVkZvcWMmx7JdA==} cpu: [x64] os: [linux] - '@nx/nx-linux-x64-musl@21.1.2': - resolution: {integrity: sha512-+0V0YAOWMh1wvpQZuayQ7y+sj2MhE3l7z0JMD9SX/4xv9zLOWGv+EiUmN/fGoU/mwsSkH2wTCo6G6quKF1E8jQ==} + '@nx/nx-linux-x64-musl@22.1.3': + resolution: {integrity: sha512-CqpRIJeIgELCqIgjtSsYnnLi6G0uqjbp/Pw9d7w4im4/NmJXqaE9gxpdHA1eowXLgAy9W1LkfzCPS8Q2IScPuQ==} cpu: [x64] os: [linux] - '@nx/nx-win32-arm64-msvc@21.1.2': - resolution: {integrity: sha512-E+ECMQIMJ6R47BMW5YpDyOhTqczvFaL8k24umRkcvlRh3SraczyxBVPkYHDukDp7tCeIszc5EvdWc83C3W8U4w==} + '@nx/nx-win32-arm64-msvc@22.1.3': + resolution: {integrity: sha512-YbuWb8KQsAR9G0+7b4HA16GV962/VWtRcdS7WY2yaScmPT2W5rObl528Y2j4DuB0j/MVZj12qJKrYfUyjL+UJA==} cpu: [arm64] os: [win32] - '@nx/nx-win32-x64-msvc@21.1.2': - resolution: {integrity: sha512-J9rNTBOS7Ld6CybU/cou1Fg52AHSYsiwpZISM2RNM0XIoVSDk3Jsvh4OJgS2rvV0Sp/cgDg3ieOMAreekH+TKw==} + '@nx/nx-win32-x64-msvc@22.1.3': + resolution: {integrity: sha512-G90Sp409ypeOUbmj6nmEbdy043KJUKaZ7pffxmM6i63yEe2F2WdmMgdi525vUEgmq+pfB9zQQOX1sDR/rPFvtg==} cpu: [x64] os: [win32] @@ -3388,6 +3557,11 @@ packages: cpu: [x64] os: [win32] + '@rsbuild/core@1.6.12': + resolution: {integrity: sha512-gHwDfAMMgK2Zu9JgiekX949F262g6yYY+/ZLTIaMXHZmqYLAgsu0XOeVogpfQa045xcnwcQfpieiB9zgukJSgw==} + engines: {node: '>=18.12.0'} + hasBin: true + '@rsbuild/core@1.6.9': resolution: {integrity: sha512-wf41bbFIzqQsGkrDal2eVC4cxN6II1k4bUo1g7OFuvWeEOJzjoeK4R5xxKM9g5hRjbGAJs6OiQaGpASvUnDrsw==} engines: {node: '>=18.12.0'} @@ -3466,8 +3640,8 @@ packages: '@rsdoctor/utils@1.0.2': resolution: {integrity: sha512-TjkZfP0jXGgWm/qFSKbGJANkYzo668fDjbr/YP/RVOKbj+ORi+3q5ieLdDghacM2coXG2xDYrYUmPPKsXU90pg==} - '@rslib/core@0.18.2': - resolution: {integrity: sha512-KIlBl8V675gzBcL17cCS5buN9wZSaS6JT7s9p1OZLOtBZTuCCu1q+5TfyTdnmFATEGgtrue4xhnX8HhAFKuMPw==} + '@rslib/core@0.18.3': + resolution: {integrity: sha512-Jdpssyig/OIJ9EzjXi957gKUCLZhlynMAunkJnaYcriFE245quLxZAdDsxXeeKjUp0Mx/J6fVXgUQyYmbKum6w==} engines: {node: '>=18.12.0'} hasBin: true peerDependencies: @@ -3484,53 +3658,105 @@ packages: cpu: [arm64] os: [darwin] + '@rspack/binding-darwin-arm64@1.6.6': + resolution: {integrity: sha512-vGVDP0rlWa2w/gLba/sncVfkCah0HmhdmK5vGj/7sSX0iViwQneA2xjxDHyCNSQrvfq9GJmj4Kmdq/9tGh0KuA==} + cpu: [arm64] + os: [darwin] + '@rspack/binding-darwin-x64@1.6.5': resolution: {integrity: sha512-fPVfp7W/GMbHayb5hbefiMI30JxlsqPexOItHGtufHmTCrNne1aHmApspyUZIUUxG36oDRHuGPnfh+IQbHR6+g==} cpu: [x64] os: [darwin] + '@rspack/binding-darwin-x64@1.6.6': + resolution: {integrity: sha512-IcdEG2kOmbPPO70Zl7gDnowDjK7d7C1hWew2vU7dPltr2t1JalRIMnS051lhiur0ULkSxV3cW1zXqv0Oi8AnOg==} + cpu: [x64] + os: [darwin] + '@rspack/binding-linux-arm64-gnu@1.6.5': resolution: {integrity: sha512-K68YDoV2e4s+nlrKZxgF0HehiiRwOAGgZFUwJNRMZ7MUrTGMNlPTJlM+bNdaCjDb6GFxBVFcNwIa1sU+0tF1zg==} cpu: [arm64] os: [linux] + '@rspack/binding-linux-arm64-gnu@1.6.6': + resolution: {integrity: sha512-rIguCCtlTcwoFlwheDiUgdImk27spuCRn43zGJogARpM/ZYRFKIuSwFDGUtJT2g0TSLUAHUhWAUqC36NwvrbMQ==} + cpu: [arm64] + os: [linux] + '@rspack/binding-linux-arm64-musl@1.6.5': resolution: {integrity: sha512-JPtxFBOq7RRmBIwpdGIStf8iyCILehDsjQtEB0Kkhtm7TsAkVGwtC41GLcNuPxcQBKqNDmD8cy3yLYhXadH2CQ==} cpu: [arm64] os: [linux] + '@rspack/binding-linux-arm64-musl@1.6.6': + resolution: {integrity: sha512-x6X6Gr0fUw6qrJGxZt3Rb6oIX+jd9pdcyp0VbtofcLaqGVQbzustYsYnuLATPOys0q4J/4kWnmEhkjLJHwkhpQ==} + cpu: [arm64] + os: [linux] + '@rspack/binding-linux-x64-gnu@1.6.5': resolution: {integrity: sha512-oh4ZNo2HtizZ/E6UK3BEONu20h8VVBw9GAXuWmo1u22cJSihzg+WfRNCMjRDil82LqSsyAgBwnU+dEjEYGKyAA==} cpu: [x64] os: [linux] + '@rspack/binding-linux-x64-gnu@1.6.6': + resolution: {integrity: sha512-gSlVdASszWHosQKn+nzYOInBijdQboUnmNMGgW9/PijVg3433IvQjzviUuJFno8CMGgrACV9yw+ZFDuK0J57VA==} + cpu: [x64] + os: [linux] + '@rspack/binding-linux-x64-musl@1.6.5': resolution: {integrity: sha512-8Xebp5bvPJqjifpkFEAX5nUvoU2JvbMU3gwAkEovRRuvooCXnVT2tqkUBjkR3AhivAGgAxAr9hRzUUz/6QWt3Q==} cpu: [x64] os: [linux] + '@rspack/binding-linux-x64-musl@1.6.6': + resolution: {integrity: sha512-TZaqVkh7memsTK/hxkOBrbpdzbmBUMea1YnYt++7QjMgco1kWFvAQ+YhAWtIaOaEg8s6C07Lt0Zp8izM2Dja0g==} + cpu: [x64] + os: [linux] + '@rspack/binding-wasm32-wasi@1.6.5': resolution: {integrity: sha512-oINZNqzTxM+9dSUOjAORodHXYoJYzXvpaHI2U6ecEmoWaBCs+x3V3Po8DhpNFBwotB+jGlcoVhEHjpg5uaO6pw==} cpu: [wasm32] + '@rspack/binding-wasm32-wasi@1.6.6': + resolution: {integrity: sha512-W4mWdlLnYrbUaktyHOGNfATblxMTbgF7CBfDw8PhbDtjd2l8e/TnaHgIDkwITHXAOMEF/QEKfo9FtusbcQJNKw==} + cpu: [wasm32] + '@rspack/binding-win32-arm64-msvc@1.6.5': resolution: {integrity: sha512-UUmep2ayuZxWPdrzkrzAFxVgYUWTB82pa9bkAGyeDO9SNkz8vTpdtbDaTvAzjFb8Pn+ErktDEDBKT57FLjxwxQ==} cpu: [arm64] os: [win32] + '@rspack/binding-win32-arm64-msvc@1.6.6': + resolution: {integrity: sha512-cw5OgxqoDwjoZlk0L3vGEwcjPZsOVFYLwr2ssiC05rsTbhBwxj8coLpAJdvUvbf6C2TTmCB7iPe2sPq1KWD37g==} + cpu: [arm64] + os: [win32] + '@rspack/binding-win32-ia32-msvc@1.6.5': resolution: {integrity: sha512-7nx+mMimpmCMstcW7nsyToXy5TK7N+YGPu2W/oioX7qv9ZCuJGTddjzLS84wN8DVrNIirg4mcxpBsmOQMZeHQA==} cpu: [ia32] os: [win32] + '@rspack/binding-win32-ia32-msvc@1.6.6': + resolution: {integrity: sha512-M4ruR+VZ59iy+mPjy6FQPT27cOgeytf3wFBrt7e0suKeNLYGxrNyI9YhgpCTY++SMJsAMgRLGDHoI3ZgWulw1Q==} + cpu: [ia32] + os: [win32] + '@rspack/binding-win32-x64-msvc@1.6.5': resolution: {integrity: sha512-pzO7rYFu6f6stgSccolZHiXGTTwKrIGHHNV1ALY1xPRmQEdbHcbMwadeaG99JL2lRLve9iNI+Z9Pr3oDVRN46g==} cpu: [x64] os: [win32] + '@rspack/binding-win32-x64-msvc@1.6.6': + resolution: {integrity: sha512-q5QTvdhPUh+CA93cQG5zWKRIHMIWPzw+ftFDEwBw52zYdvNAoLniqD8o5Mi8CT0pndhulXgR5aw0Sjd3eMah+A==} + cpu: [x64] + os: [win32] + '@rspack/binding@1.6.5': resolution: {integrity: sha512-FzYsr5vdjaVQIlDTxZFlISOQGxl/4grpF2BeiNy60Fpw9eeADeXk55DVacbXPqpiz7Doj6cyhEyMszQOvihrqQ==} + '@rspack/binding@1.6.6': + resolution: {integrity: sha512-noiV+qhyBTVpvG2M4bnOwKk2Ynl6G47Wf7wpCjPCFr87qr3txNwTTnhkEJEU59yj+VvIhbRD2rf5+9TLoT0Wxg==} + '@rspack/core@1.6.5': resolution: {integrity: sha512-AqaOMA6MTNhqMYYwrhvPA+2uS662SkAi8Rb7B/IFOzh/Z5ooyczL4lUX+qyhAO3ymn50iwM4jikQCf9XfBiaQA==} engines: {node: '>=18.12.0'} @@ -3540,9 +3766,14 @@ packages: '@swc/helpers': optional: true - '@rspack/lite-tapable@1.0.1': - resolution: {integrity: sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==} - engines: {node: '>=16.0.0'} + '@rspack/core@1.6.6': + resolution: {integrity: sha512-2mR+2YBydlgZ7Q0Rpd6bCC3MBnV9TS0x857K0zIhbDj4BQOqaWVy1n7fx/B3MrS8TR0QCuzKfyDAjNz+XTyJVQ==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@swc/helpers': + optional: true '@rspack/lite-tapable@1.1.0': resolution: {integrity: sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==} @@ -3699,8 +3930,8 @@ packages: '@silvia-odwyer/photon@0.3.3': resolution: {integrity: sha512-8BhUjEch4slwRe8uXnaA4vcA5uiiOTT90UMsxulOj2gN98X1p0q9Z4Ysk4DkD05uNgbR9XoSqtZ37w+33w4QKQ==} - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sinclair/typebox@0.34.41': + resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} @@ -5482,10 +5713,6 @@ packages: devtools-protocol@0.0.1402036: resolution: {integrity: sha512-JwAYQgEvm3yD45CHB+RmF5kMbWtXBaOGwuxa87sZogHcLCv8c/IqnThaoQ1y60d7pXWjSKWQphPEc+1rAScVdg==} - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -5830,18 +6057,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - eventsource-parser@3.0.1: - resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==} - engines: {node: '>=18.0.0'} - eventsource-parser@3.0.3: resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} engines: {node: '>=20.0.0'} - eventsource@3.0.6: - resolution: {integrity: sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==} - engines: {node: '>=18.0.0'} - eventsource@3.0.7: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} @@ -5868,12 +6087,6 @@ packages: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} - express-rate-limit@7.5.0: - resolution: {integrity: sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==} - engines: {node: '>= 16'} - peerDependencies: - express: ^4.11 || 5 || ^5.0.0-beta.1 - express-rate-limit@7.5.1: resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} engines: {node: '>= 16'} @@ -6524,6 +6737,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + image-q@4.0.0: resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} @@ -6922,13 +7139,9 @@ packages: resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==} engines: {node: 20 || >=22} - jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-diff@30.2.0: + resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} @@ -7828,8 +8041,8 @@ packages: nwsapi@2.2.22: resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} - nx@21.1.2: - resolution: {integrity: sha512-oczAEOOkQHElxCXs2g2jXDRabDRsmub/h5SAgqAUDSJ2CRnYGVVlgZX7l+o+A9kSqfONyLy5FlJ1pSWlvPuG4w==} + nx@22.1.3: + resolution: {integrity: sha512-8zS/jhz1ZYSlW3tDEkqIA3oXaS/BTnpuFNV6L3tGKAaIxdn1sD5BuOdxIVK+G/TaoxOhw2iKrGiZeSSpV7fILw==} hasBin: true peerDependencies: '@swc-node/register': ^1.8.0 @@ -8287,9 +8500,9 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-format@30.2.0: + resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} pretty-ms@9.1.0: resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} @@ -8966,8 +9179,8 @@ packages: rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} - rsbuild-plugin-dts@0.18.2: - resolution: {integrity: sha512-gUZ6MXjp7PQtBWlyCkcNp34scgc7qSQRc6Rw4YHVeuBnLvVAXsToYJHn32ImYPBJzRGFA9dz5D1r7ZZKurD7Vg==} + rsbuild-plugin-dts@0.18.3: + resolution: {integrity: sha512-+V0D37pK0Q+5DIVGDlq+ky5H/Sb2VnTzsbtV3l0Hw9tVcXQOjEQMeWAmBgC8jBgNjnCVEWrwCqLdAvSTzIeOOw==} engines: {node: '>=18.12.0'} peerDependencies: '@microsoft/api-extractor': ^7 @@ -10567,11 +10780,6 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} - zod-to-json-schema@3.24.5: - resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} - peerDependencies: - zod: ^3.24.1 - zod-to-json-schema@3.24.6: resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} peerDependencies: @@ -11961,9 +12169,13 @@ snapshots: dependencies: minipass: 7.1.2 - '@jest/schemas@29.6.3': + '@jest/diff-sequences@30.0.1': {} + + '@jest/get-type@30.1.0': {} + + '@jest/schemas@30.0.5': dependencies: - '@sinclair/typebox': 0.27.8 + '@sinclair/typebox': 0.34.41 '@jimp/bmp@0.22.12(@jimp/custom@0.22.12)': dependencies: @@ -12183,7 +12395,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.12': dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.29 '@jridgewell/gen-mapping@0.3.13': @@ -12198,14 +12410,12 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/sourcemap-codec@1.5.4': {} - '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.29': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping@0.3.31': dependencies: @@ -12215,7 +12425,7 @@ snapshots: '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 '@jsdevtools/ez-spawn@3.0.4': dependencies: @@ -12453,7 +12663,31 @@ snapshots: - supports-color - utf-8-validate - '@modelcontextprotocol/inspector@0.16.3(@types/node@18.19.62)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5)': + '@modelcontextprotocol/inspector@0.16.3(@types/node@18.19.118)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@6.0.5)': + dependencies: + '@modelcontextprotocol/inspector-cli': 0.16.3 + '@modelcontextprotocol/inspector-client': 0.16.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5) + '@modelcontextprotocol/inspector-server': 0.16.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) + '@modelcontextprotocol/sdk': 1.17.2 + concurrently: 9.2.0 + open: 10.2.0 + shell-quote: 1.8.3 + spawn-rx: 5.1.2 + ts-node: 10.9.2(@types/node@18.19.118)(typescript@5.8.3) + zod: 3.25.76 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + - '@types/react' + - '@types/react-dom' + - bufferutil + - supports-color + - tailwindcss + - typescript + - utf-8-validate + + '@modelcontextprotocol/inspector@0.16.3(@types/node@18.19.62)(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(typescript@5.8.3)': dependencies: '@modelcontextprotocol/inspector-cli': 0.16.3 '@modelcontextprotocol/inspector-client': 0.16.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5) @@ -12482,13 +12716,13 @@ snapshots: content-type: 1.0.5 cors: 2.8.5 cross-spawn: 7.0.6 - eventsource: 3.0.6 + eventsource: 3.0.7 express: 5.1.0 - express-rate-limit: 7.5.0(express@5.1.0) + express-rate-limit: 7.5.1(express@5.1.0) pkce-challenge: 5.0.0 raw-body: 3.0.0 zod: 3.24.3 - zod-to-json-schema: 3.24.5(zod@3.24.3) + zod-to-json-schema: 3.24.6(zod@3.24.3) transitivePeerDependencies: - supports-color @@ -12511,29 +12745,54 @@ snapshots: '@module-federation/error-codes@0.21.4': {} + '@module-federation/error-codes@0.21.6': {} + '@module-federation/runtime-core@0.21.4': dependencies: '@module-federation/error-codes': 0.21.4 '@module-federation/sdk': 0.21.4 + '@module-federation/runtime-core@0.21.6': + dependencies: + '@module-federation/error-codes': 0.21.6 + '@module-federation/sdk': 0.21.6 + '@module-federation/runtime-tools@0.21.4': dependencies: '@module-federation/runtime': 0.21.4 '@module-federation/webpack-bundler-runtime': 0.21.4 + '@module-federation/runtime-tools@0.21.6': + dependencies: + '@module-federation/runtime': 0.21.6 + '@module-federation/webpack-bundler-runtime': 0.21.6 + '@module-federation/runtime@0.21.4': dependencies: '@module-federation/error-codes': 0.21.4 '@module-federation/runtime-core': 0.21.4 '@module-federation/sdk': 0.21.4 + '@module-federation/runtime@0.21.6': + dependencies: + '@module-federation/error-codes': 0.21.6 + '@module-federation/runtime-core': 0.21.6 + '@module-federation/sdk': 0.21.6 + '@module-federation/sdk@0.21.4': {} + '@module-federation/sdk@0.21.6': {} + '@module-federation/webpack-bundler-runtime@0.21.4': dependencies: '@module-federation/runtime': 0.21.4 '@module-federation/sdk': 0.21.4 + '@module-federation/webpack-bundler-runtime@0.21.6': + dependencies: + '@module-federation/runtime': 0.21.6 + '@module-federation/sdk': 0.21.6 + '@napi-rs/wasm-runtime@0.2.4': dependencies: '@emnapi/core': 1.7.1 @@ -12559,34 +12818,34 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nx/nx-darwin-arm64@21.1.2': + '@nx/nx-darwin-arm64@22.1.3': optional: true - '@nx/nx-darwin-x64@21.1.2': + '@nx/nx-darwin-x64@22.1.3': optional: true - '@nx/nx-freebsd-x64@21.1.2': + '@nx/nx-freebsd-x64@22.1.3': optional: true - '@nx/nx-linux-arm-gnueabihf@21.1.2': + '@nx/nx-linux-arm-gnueabihf@22.1.3': optional: true - '@nx/nx-linux-arm64-gnu@21.1.2': + '@nx/nx-linux-arm64-gnu@22.1.3': optional: true - '@nx/nx-linux-arm64-musl@21.1.2': + '@nx/nx-linux-arm64-musl@22.1.3': optional: true - '@nx/nx-linux-x64-gnu@21.1.2': + '@nx/nx-linux-x64-gnu@22.1.3': optional: true - '@nx/nx-linux-x64-musl@21.1.2': + '@nx/nx-linux-x64-musl@22.1.3': optional: true - '@nx/nx-win32-arm64-msvc@21.1.2': + '@nx/nx-win32-arm64-msvc@22.1.3': optional: true - '@nx/nx-win32-x64-msvc@21.1.2': + '@nx/nx-win32-x64-msvc@22.1.3': optional: true '@pixi/color@7.4.3': @@ -13173,6 +13432,14 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.24.3': optional: true + '@rsbuild/core@1.6.12': + dependencies: + '@rspack/core': 1.6.6(@swc/helpers@0.5.17) + '@rspack/lite-tapable': 1.1.0 + '@swc/helpers': 0.5.17 + core-js: 3.47.0 + jiti: 2.6.1 + '@rsbuild/core@1.6.9': dependencies: '@rspack/core': 1.6.5(@swc/helpers@0.5.17) @@ -13191,12 +13458,46 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.6.9 + '@rsbuild/plugin-less@1.5.0(@rsbuild/core@1.6.12)': + dependencies: + '@rsbuild/core': 1.6.12 + deepmerge: 4.3.1 + reduce-configs: 1.1.1 + '@rsbuild/plugin-less@1.5.0(@rsbuild/core@1.6.9)': dependencies: '@rsbuild/core': 1.6.9 deepmerge: 4.3.1 reduce-configs: 1.1.1 + '@rsbuild/plugin-node-polyfill@1.4.2(@rsbuild/core@1.6.12)': + dependencies: + assert: 2.1.0 + browserify-zlib: 0.2.0 + buffer: 5.7.1 + console-browserify: 1.2.0 + constants-browserify: 1.0.0 + crypto-browserify: 3.12.1 + domain-browser: 5.7.0 + events: 3.3.0 + https-browserify: 1.0.0 + os-browserify: 0.3.0 + path-browserify: 1.0.1 + process: 0.11.10 + punycode: 2.3.1 + querystring-es3: 0.2.1 + readable-stream: 4.7.0 + stream-browserify: 3.0.0 + stream-http: 3.2.0 + string_decoder: 1.3.0 + timers-browserify: 2.0.12 + tty-browserify: 0.0.1 + url: 0.11.4 + util: 0.12.5 + vm-browserify: 1.1.2 + optionalDependencies: + '@rsbuild/core': 1.6.12 + '@rsbuild/plugin-node-polyfill@1.4.2(@rsbuild/core@1.6.9)': dependencies: assert: 2.1.0 @@ -13225,6 +13526,14 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.6.9 + '@rsbuild/plugin-react@1.4.1(@rsbuild/core@1.6.12)': + dependencies: + '@rsbuild/core': 1.6.12 + '@rspack/plugin-react-refresh': 1.5.1(react-refresh@0.17.0) + react-refresh: 0.17.0 + transitivePeerDependencies: + - webpack-hot-middleware + '@rsbuild/plugin-react@1.4.1(@rsbuild/core@1.6.9)': dependencies: '@rsbuild/core': 1.6.9 @@ -13241,6 +13550,20 @@ snapshots: transitivePeerDependencies: - webpack-hot-middleware + '@rsbuild/plugin-svgr@1.2.2(@rsbuild/core@1.6.12)(typescript@5.8.3)': + dependencies: + '@rsbuild/core': 1.6.12 + '@rsbuild/plugin-react': 1.4.1(@rsbuild/core@1.6.12) + '@svgr/core': 8.1.0(typescript@5.8.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3))(typescript@5.8.3) + deepmerge: 4.3.1 + loader-utils: 3.3.1 + transitivePeerDependencies: + - supports-color + - typescript + - webpack-hot-middleware + '@rsbuild/plugin-svgr@1.2.2(@rsbuild/core@1.6.9)(typescript@5.8.3)': dependencies: '@rsbuild/core': 1.6.9 @@ -13255,12 +13578,12 @@ snapshots: - typescript - webpack-hot-middleware - '@rsbuild/plugin-type-check@1.2.4(@rsbuild/core@1.6.9)(@rspack/core@1.6.5)(typescript@5.8.3)': + '@rsbuild/plugin-type-check@1.2.4(@rsbuild/core@1.6.9)(@rspack/core@1.6.6)(typescript@5.8.3)': dependencies: deepmerge: 4.3.1 json5: 2.2.3 reduce-configs: 1.1.1 - ts-checker-rspack-plugin: 1.1.4(@rspack/core@1.6.5)(typescript@5.8.3) + ts-checker-rspack-plugin: 1.1.4(@rspack/core@1.6.6)(typescript@5.8.3) optionalDependencies: '@rsbuild/core': 1.6.9 transitivePeerDependencies: @@ -13269,13 +13592,13 @@ snapshots: '@rsdoctor/client@1.0.2': {} - '@rsdoctor/core@1.0.2(@rsbuild/core@1.6.9)(@rspack/core@1.6.5(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5)': + '@rsdoctor/core@1.0.2(@rsbuild/core@1.6.9)(@rspack/core@1.6.6(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5)': dependencies: '@rsbuild/plugin-check-syntax': 1.3.0(@rsbuild/core@1.6.9) - '@rsdoctor/graph': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) - '@rsdoctor/sdk': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) - '@rsdoctor/types': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(webpack@5.99.5) - '@rsdoctor/utils': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(webpack@5.99.5) + '@rsdoctor/graph': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) + '@rsdoctor/sdk': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) + '@rsdoctor/types': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(webpack@5.99.5) + '@rsdoctor/utils': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(webpack@5.99.5) axios: 1.9.0 browserslist-load-config: 1.0.0 enhanced-resolve: 5.12.0 @@ -13295,10 +13618,10 @@ snapshots: - utf-8-validate - webpack - '@rsdoctor/graph@1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5)': + '@rsdoctor/graph@1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5)': dependencies: - '@rsdoctor/types': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(webpack@5.99.5) - '@rsdoctor/utils': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(webpack@5.99.5) + '@rsdoctor/types': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(webpack@5.99.5) + '@rsdoctor/utils': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(webpack@5.99.5) lodash.unionby: 4.8.0 socket.io: 4.8.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) source-map: 0.7.4 @@ -13309,14 +13632,14 @@ snapshots: - utf-8-validate - webpack - '@rsdoctor/rspack-plugin@1.0.2(@rsbuild/core@1.6.9)(@rspack/core@1.6.5(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5)': + '@rsdoctor/rspack-plugin@1.0.2(@rsbuild/core@1.6.9)(@rspack/core@1.6.6(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5)': dependencies: - '@rsdoctor/core': 1.0.2(@rsbuild/core@1.6.9)(@rspack/core@1.6.5(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) - '@rsdoctor/graph': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) - '@rsdoctor/sdk': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) - '@rsdoctor/types': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(webpack@5.99.5) - '@rsdoctor/utils': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(webpack@5.99.5) - '@rspack/core': 1.6.5(@swc/helpers@0.5.17) + '@rsdoctor/core': 1.0.2(@rsbuild/core@1.6.9)(@rspack/core@1.6.6(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) + '@rsdoctor/graph': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) + '@rsdoctor/sdk': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) + '@rsdoctor/types': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(webpack@5.99.5) + '@rsdoctor/utils': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(webpack@5.99.5) + '@rspack/core': 1.6.6(@swc/helpers@0.5.17) lodash: 4.17.21 transitivePeerDependencies: - '@rsbuild/core' @@ -13326,12 +13649,12 @@ snapshots: - utf-8-validate - webpack - '@rsdoctor/sdk@1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5)': + '@rsdoctor/sdk@1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5)': dependencies: '@rsdoctor/client': 1.0.2 - '@rsdoctor/graph': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) - '@rsdoctor/types': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(webpack@5.99.5) - '@rsdoctor/utils': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(webpack@5.99.5) + '@rsdoctor/graph': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(bufferutil@4.0.9)(webpack@5.99.5) + '@rsdoctor/types': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(webpack@5.99.5) + '@rsdoctor/utils': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(webpack@5.99.5) '@types/fs-extra': 11.0.4 body-parser: 1.20.3 cors: 2.8.5 @@ -13351,7 +13674,7 @@ snapshots: - utf-8-validate - webpack - '@rsdoctor/types@1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(webpack@5.99.5)': + '@rsdoctor/types@1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(webpack@5.99.5)': dependencies: '@types/connect': 3.4.38 '@types/estree': 1.0.5 @@ -13359,12 +13682,12 @@ snapshots: source-map: 0.7.4 webpack: 5.99.5 optionalDependencies: - '@rspack/core': 1.6.5(@swc/helpers@0.5.17) + '@rspack/core': 1.6.6(@swc/helpers@0.5.17) - '@rsdoctor/utils@1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(webpack@5.99.5)': + '@rsdoctor/utils@1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(webpack@5.99.5)': dependencies: '@babel/code-frame': 7.26.2 - '@rsdoctor/types': 1.0.2(@rspack/core@1.6.5(@swc/helpers@0.5.17))(webpack@5.99.5) + '@rsdoctor/types': 1.0.2(@rspack/core@1.6.6(@swc/helpers@0.5.17))(webpack@5.99.5) '@types/estree': 1.0.5 acorn: 8.15.0 acorn-import-attributes: 1.9.5(acorn@8.15.0) @@ -13384,30 +13707,30 @@ snapshots: - '@rspack/core' - webpack - '@rslib/core@0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3)': + '@rslib/core@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(typescript@5.8.3)': dependencies: - '@rsbuild/core': 1.6.9 - rsbuild-plugin-dts: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(@rsbuild/core@1.6.9)(typescript@5.8.3) + '@rsbuild/core': 1.6.12 + rsbuild-plugin-dts: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(@rsbuild/core@1.6.12)(typescript@5.8.3) optionalDependencies: '@microsoft/api-extractor': 7.52.10(@types/node@18.19.118) typescript: 5.8.3 transitivePeerDependencies: - '@typescript/native-preview' - '@rslib/core@0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3)': + '@rslib/core@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3)': dependencies: - '@rsbuild/core': 1.6.9 - rsbuild-plugin-dts: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(@rsbuild/core@1.6.9)(typescript@5.8.3) + '@rsbuild/core': 1.6.12 + rsbuild-plugin-dts: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(@rsbuild/core@1.6.12)(typescript@5.8.3) optionalDependencies: '@microsoft/api-extractor': 7.52.10(@types/node@18.19.62) typescript: 5.8.3 transitivePeerDependencies: - '@typescript/native-preview' - '@rslib/core@0.18.2(@microsoft/api-extractor@7.52.10(@types/node@24.10.1))(typescript@5.8.3)': + '@rslib/core@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@24.10.1))(typescript@5.8.3)': dependencies: - '@rsbuild/core': 1.6.9 - rsbuild-plugin-dts: 0.18.2(@microsoft/api-extractor@7.52.10(@types/node@24.10.1))(@rsbuild/core@1.6.9)(typescript@5.8.3) + '@rsbuild/core': 1.6.12 + rsbuild-plugin-dts: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@24.10.1))(@rsbuild/core@1.6.12)(typescript@5.8.3) optionalDependencies: '@microsoft/api-extractor': 7.52.10(@types/node@24.10.1) typescript: 5.8.3 @@ -13417,35 +13740,67 @@ snapshots: '@rspack/binding-darwin-arm64@1.6.5': optional: true + '@rspack/binding-darwin-arm64@1.6.6': + optional: true + '@rspack/binding-darwin-x64@1.6.5': optional: true + '@rspack/binding-darwin-x64@1.6.6': + optional: true + '@rspack/binding-linux-arm64-gnu@1.6.5': optional: true + '@rspack/binding-linux-arm64-gnu@1.6.6': + optional: true + '@rspack/binding-linux-arm64-musl@1.6.5': optional: true + '@rspack/binding-linux-arm64-musl@1.6.6': + optional: true + '@rspack/binding-linux-x64-gnu@1.6.5': optional: true + '@rspack/binding-linux-x64-gnu@1.6.6': + optional: true + '@rspack/binding-linux-x64-musl@1.6.5': optional: true + '@rspack/binding-linux-x64-musl@1.6.6': + optional: true + '@rspack/binding-wasm32-wasi@1.6.5': dependencies: '@napi-rs/wasm-runtime': 1.0.7 optional: true + '@rspack/binding-wasm32-wasi@1.6.6': + dependencies: + '@napi-rs/wasm-runtime': 1.0.7 + optional: true + '@rspack/binding-win32-arm64-msvc@1.6.5': optional: true + '@rspack/binding-win32-arm64-msvc@1.6.6': + optional: true + '@rspack/binding-win32-ia32-msvc@1.6.5': optional: true + '@rspack/binding-win32-ia32-msvc@1.6.6': + optional: true + '@rspack/binding-win32-x64-msvc@1.6.5': optional: true + '@rspack/binding-win32-x64-msvc@1.6.6': + optional: true + '@rspack/binding@1.6.5': optionalDependencies: '@rspack/binding-darwin-arm64': 1.6.5 @@ -13459,6 +13814,19 @@ snapshots: '@rspack/binding-win32-ia32-msvc': 1.6.5 '@rspack/binding-win32-x64-msvc': 1.6.5 + '@rspack/binding@1.6.6': + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.6.6 + '@rspack/binding-darwin-x64': 1.6.6 + '@rspack/binding-linux-arm64-gnu': 1.6.6 + '@rspack/binding-linux-arm64-musl': 1.6.6 + '@rspack/binding-linux-x64-gnu': 1.6.6 + '@rspack/binding-linux-x64-musl': 1.6.6 + '@rspack/binding-wasm32-wasi': 1.6.6 + '@rspack/binding-win32-arm64-msvc': 1.6.6 + '@rspack/binding-win32-ia32-msvc': 1.6.6 + '@rspack/binding-win32-x64-msvc': 1.6.6 + '@rspack/core@1.6.5(@swc/helpers@0.5.17)': dependencies: '@module-federation/runtime-tools': 0.21.4 @@ -13467,7 +13835,13 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.17 - '@rspack/lite-tapable@1.0.1': {} + '@rspack/core@1.6.6(@swc/helpers@0.5.17)': + dependencies: + '@module-federation/runtime-tools': 0.21.6 + '@rspack/binding': 1.6.6 + '@rspack/lite-tapable': 1.1.0 + optionalDependencies: + '@swc/helpers': 0.5.17 '@rspack/lite-tapable@1.1.0': {} @@ -13753,7 +14127,7 @@ snapshots: '@silvia-odwyer/photon@0.3.3': {} - '@sinclair/typebox@0.27.8': {} + '@sinclair/typebox@0.34.41': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -15811,8 +16185,6 @@ snapshots: devtools-protocol@0.0.1402036: {} - diff-sequences@29.6.3: {} - diff@4.0.2: {} diffie-hellman@5.0.3: @@ -16308,14 +16680,8 @@ snapshots: events@3.3.0: {} - eventsource-parser@3.0.1: {} - eventsource-parser@3.0.3: {} - eventsource@3.0.6: - dependencies: - eventsource-parser: 3.0.1 - eventsource@3.0.7: dependencies: eventsource-parser: 3.0.3 @@ -16360,10 +16726,6 @@ snapshots: expect-type@1.2.1: {} - express-rate-limit@7.5.0(express@5.1.0): - dependencies: - express: 5.1.0 - express-rate-limit@7.5.1(express@5.1.0): dependencies: express: 5.1.0 @@ -16631,7 +16993,7 @@ snapshots: foreground-child@3.3.0: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 signal-exit: 4.1.0 form-data@4.0.1: @@ -17241,6 +17603,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + image-q@4.0.0: dependencies: '@types/node': 16.9.1 @@ -17605,14 +17969,12 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 - jest-diff@29.7.0: + jest-diff@30.2.0: dependencies: + '@jest/diff-sequences': 30.0.1 + '@jest/get-type': 30.1.0 chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-get-type@29.6.3: {} + pretty-format: 30.2.0 jest-worker@27.5.1: dependencies: @@ -18006,7 +18368,7 @@ snapshots: magic-string@0.30.17: dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 make-dir@2.1.0: dependencies: @@ -18728,7 +19090,7 @@ snapshots: nwsapi@2.2.22: optional: true - nx@21.1.2: + nx@22.1.3: dependencies: '@napi-rs/wasm-runtime': 0.2.4 '@yarnpkg/lockfile': 1.1.0 @@ -18745,8 +19107,8 @@ snapshots: figures: 3.2.0 flat: 5.0.2 front-matter: 4.0.2 - ignore: 5.3.2 - jest-diff: 29.7.0 + ignore: 7.0.5 + jest-diff: 30.2.0 jsonc-parser: 3.2.0 lines-and-columns: 2.0.3 minimatch: 9.0.3 @@ -18766,16 +19128,16 @@ snapshots: yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@nx/nx-darwin-arm64': 21.1.2 - '@nx/nx-darwin-x64': 21.1.2 - '@nx/nx-freebsd-x64': 21.1.2 - '@nx/nx-linux-arm-gnueabihf': 21.1.2 - '@nx/nx-linux-arm64-gnu': 21.1.2 - '@nx/nx-linux-arm64-musl': 21.1.2 - '@nx/nx-linux-x64-gnu': 21.1.2 - '@nx/nx-linux-x64-musl': 21.1.2 - '@nx/nx-win32-arm64-msvc': 21.1.2 - '@nx/nx-win32-x64-msvc': 21.1.2 + '@nx/nx-darwin-arm64': 22.1.3 + '@nx/nx-darwin-x64': 22.1.3 + '@nx/nx-freebsd-x64': 22.1.3 + '@nx/nx-linux-arm-gnueabihf': 22.1.3 + '@nx/nx-linux-arm64-gnu': 22.1.3 + '@nx/nx-linux-arm64-musl': 22.1.3 + '@nx/nx-linux-x64-gnu': 22.1.3 + '@nx/nx-linux-x64-musl': 22.1.3 + '@nx/nx-win32-arm64-msvc': 22.1.3 + '@nx/nx-win32-x64-msvc': 22.1.3 transitivePeerDependencies: - debug @@ -19244,9 +19606,9 @@ snapshots: prettier@3.7.4: {} - pretty-format@29.7.0: + pretty-format@30.2.0: dependencies: - '@jest/schemas': 29.6.3 + '@jest/schemas': 30.0.5 ansi-styles: 5.2.0 react-is: 18.3.1 @@ -20134,26 +20496,26 @@ snapshots: rrweb-cssom@0.8.0: optional: true - rsbuild-plugin-dts@0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(@rsbuild/core@1.6.9)(typescript@5.8.3): + rsbuild-plugin-dts@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.118))(@rsbuild/core@1.6.12)(typescript@5.8.3): dependencies: '@ast-grep/napi': 0.37.0 - '@rsbuild/core': 1.6.9 + '@rsbuild/core': 1.6.12 optionalDependencies: '@microsoft/api-extractor': 7.52.10(@types/node@18.19.118) typescript: 5.8.3 - rsbuild-plugin-dts@0.18.2(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(@rsbuild/core@1.6.9)(typescript@5.8.3): + rsbuild-plugin-dts@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(@rsbuild/core@1.6.12)(typescript@5.8.3): dependencies: '@ast-grep/napi': 0.37.0 - '@rsbuild/core': 1.6.9 + '@rsbuild/core': 1.6.12 optionalDependencies: '@microsoft/api-extractor': 7.52.10(@types/node@18.19.62) typescript: 5.8.3 - rsbuild-plugin-dts@0.18.2(@microsoft/api-extractor@7.52.10(@types/node@24.10.1))(@rsbuild/core@1.6.9)(typescript@5.8.3): + rsbuild-plugin-dts@0.18.3(@microsoft/api-extractor@7.52.10(@types/node@24.10.1))(@rsbuild/core@1.6.12)(typescript@5.8.3): dependencies: '@ast-grep/napi': 0.37.0 - '@rsbuild/core': 1.6.9 + '@rsbuild/core': 1.6.12 optionalDependencies: '@microsoft/api-extractor': 7.52.10(@types/node@24.10.1) typescript: 5.8.3 @@ -21124,10 +21486,10 @@ snapshots: dependencies: utf8-byte-length: 1.0.5 - ts-checker-rspack-plugin@1.1.4(@rspack/core@1.6.5)(typescript@5.8.3): + ts-checker-rspack-plugin@1.1.4(@rspack/core@1.6.6)(typescript@5.8.3): dependencies: '@babel/code-frame': 7.27.1 - '@rspack/lite-tapable': 1.0.1 + '@rspack/lite-tapable': 1.1.0 chokidar: 3.6.0 is-glob: 4.0.3 memfs: 4.17.2 @@ -21135,7 +21497,25 @@ snapshots: picocolors: 1.1.1 typescript: 5.8.3 optionalDependencies: - '@rspack/core': 1.6.5(@swc/helpers@0.5.17) + '@rspack/core': 1.6.6(@swc/helpers@0.5.17) + + ts-node@10.9.2(@types/node@18.19.118)(typescript@5.8.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.19.118 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.8.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 ts-node@10.9.2(@types/node@18.19.62)(typescript@5.8.3): dependencies: @@ -21538,7 +21918,7 @@ snapshots: sass-embedded: 1.86.3 terser: 5.44.1 - vitest@3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@26.1.0)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1): + vitest@3.0.5(@types/debug@4.1.12)(@types/node@18.19.118)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1): dependencies: '@vitest/expect': 3.0.5 '@vitest/mocker': 3.0.5(vite@5.4.10(@types/node@18.19.118)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.44.1)) @@ -22036,10 +22416,6 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod-to-json-schema@3.24.5(zod@3.24.3): - dependencies: - zod: 3.24.3 - zod-to-json-schema@3.24.6(zod@3.24.3): dependencies: zod: 3.24.3