diff --git a/CONFIG.md b/CONFIG.md
deleted file mode 100644
index aa7abf4..0000000
--- a/CONFIG.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# MCP Appium Configuration Guide
-
-## Supabase Tracing Setup
-
-### Option 1: Environment Variables (Recommended)
-Create a `.env` file in your project root:
-```bash
-SUPABASE_URL=https://your-project-id.supabase.co
-SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
-```
-
-### Option 2: Shell Environment
-```bash
-export SUPABASE_URL="https://your-project-id.supabase.co"
-export SUPABASE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
-```
-
-### Option 3: Inline Environment
-```bash
-SUPABASE_URL="https://your-project-id.supabase.co" SUPABASE_KEY="eyJ..." npm start
-```
-
-### Option 4: Configuration File
-Create a `config.json` file:
-```json
-{
- "tracing": {
- "enabled": true,
- "supabase": {
- "url": "https://your-project-id.supabase.co",
- "key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
- }
- }
-}
-```
-
-## Security Best Practices
-
-1. **Never commit credentials to version control**
-2. **Use environment variables for production**
-3. **Rotate keys regularly**
-4. **Use least-privilege access**
-5. **Monitor usage and set up alerts**
-
-## Getting Supabase Credentials
-
-1. Go to [supabase.com](https://supabase.com)
-2. Create or select your project
-3. Navigate to Settings > API
-4. Copy your Project URL and anon/public key
-5. Set up Row Level Security (RLS) policies as needed
diff --git a/README.md b/README.md
index 2a52c00..b07302d 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,6 @@
-
-

-
+# MCP Appium - MCP server for Mobile Development and Automation | iOS, Android, Simulator, Emulator, and Real Devices
-# Jarvis Appium - MCP server for Mobile Development and Automation | iOS, Android, Simulator, Emulator, and Real Devices
-
-Jarvis Appium is an intelligent MCP (Model Context Protocol) server designed to empower AI assistants with a robust suite of tools for mobile automation. It streamlines mobile app testing by enabling natural language interactions, intelligent locator generation, and automated test creation for both Android and iOS platforms.
+MCP Appium is an intelligent MCP (Model Context Protocol) server designed to empower AI assistants with a robust suite of tools for mobile automation. It streamlines mobile app testing by enabling natural language interactions, intelligent locator generation, and automated test creation for both Android and iOS platforms.
## Table of Contents
@@ -21,11 +17,9 @@ Jarvis Appium is an intelligent MCP (Model Context Protocol) server designed to
## 🚀 Features
- **Cross-Platform Support**: Automate tests for both Android (UiAutomator2) and iOS (XCUITest).
-- **Cloud Integration**: Seamlessly connect with the LambdaTest cloud platform for scalable testing.
- **Intelligent Locator Generation**: AI-powered element identification using priority-based strategies.
-- **Interactive Session Management**: Easily create and manage sessions on local and cloud-based mobile devices.
+- **Interactive Session Management**: Easily create and manage sessions on local mobile devices.
- **Smart Element Interactions**: Perform actions like clicks, text input, screenshots, and element finding.
-- **App Management**: Upload and manage mobile applications on cloud platforms.
- **Automated Test Generation**: Generate Java/TestNG test code from natural language descriptions.
- **Page Object Model Support**: Utilize built-in templates that follow industry best practices.
- **Flexible Configuration**: Customize capabilities and settings for different environments.
@@ -63,17 +57,17 @@ Before you begin, ensure you have the following installed:
### As an MCP Server
-To integrate Jarvis Appium with your MCP client, add the following to your configuration:
+To integrate MCP Appium with your MCP client, add the following to your configuration:
```json
{
"mcpServers": {
- "jarvis-appium": {
+ "mcp-appium": {
"disabled": false,
"timeout": 100,
"type": "stdio",
"command": "npx",
- "args": ["jarvis-appium"],
+ "args": ["mcp-appium"],
"env": {
"ANDROID_HOME": "/path/to/android/sdk",
"CAPABILITIES_CONFIG": "/path/to/your/capabilities.json"
@@ -114,31 +108,29 @@ Set the `CAPABILITIES_CONFIG` environment variable to point to your configuratio
### Session Management
-- `select_platform`: Choose between "android" or "ios".
-- `create_session`: Create a new mobile automation session.
-- `create_lambdatest_session`: Create a session on the LambdaTest cloud platform.
-- `upload_app_lambdatest`: Upload a mobile app to LambdaTest.
-- `appium_activate_app`: Activate a specified app.
-- `appium_terminate_app`: Terminate a specified app.
+- `select_platform`: Choose between "android" or "ios".
+- `create_session`: Create a new mobile automation session.
+- `appium_activate_app`: Activate a specified app.
+- `appium_terminate_app`: Terminate a specified app.
### Element Interaction
-- `generate_locators`: Generate intelligent locators for all interactive elements on the current screen.
-- `appium_find_element`: Find a specific element using various locator strategies.
-- `appium_click`: Click on an element.
-- `appium_set_value`: Enter text into an input field.
-- `appium_get_text`: Retrieve the text content of an element.
-- `appium_screenshot`: Capture a screenshot of the current screen.
-- `appium_scroll`: Scroll the screen vertically.
-- `appium_scroll_to_element`: Scroll until a specific element is found.
+- `generate_locators`: Generate intelligent locators for all interactive elements on the current screen.
+- `appium_find_element`: Find a specific element using various locator strategies.
+- `appium_click`: Click on an element.
+- `appium_set_value`: Enter text into an input field.
+- `appium_get_text`: Retrieve the text content of an element.
+- `appium_screenshot`: Capture a screenshot of the current screen.
+- `appium_scroll`: Scroll the screen vertically.
+- `appium_scroll_to_element`: Scroll until a specific element is found.
### Test Generation
-- `appium_generate_tests`: Generate automated test code from natural language scenarios.
+- `appium_generate_tests`: Generate automated test code from natural language scenarios.
## 🤖 Client Support
-Jarvis Appium is designed to be compatible with any MCP-compliant client.
+MCP Appium is designed to be compatible with any MCP-compliant client.
## 📚 Usage Examples
@@ -150,26 +142,7 @@ Here's an example prompt to test the Amazon mobile app checkout process:
Open Amazon mobile app, search for "iPhone 15 Pro", select the first search result, add the item to cart, proceed to checkout, sign in with email "test@example.com" and password "testpassword123", select shipping address, choose payment method, review order details, and place the order. Use JAVA + TestNG for test generation.
```
-This example demonstrates a complete e-commerce checkout flow that can be automated using Jarvis Appium's intelligent locator generation and test creation capabilities.
-
-```
-
-### LambdaTest Cloud Testing
-
-1. **Upload Your App**:
- ```
- Use upload_app_lambdatest with:
- - appPath: "/path/to/your/app.apk"
- - appName: "My Test App"
- ```
-2. **Create Cloud Session**:
- ```
- Use create_lambdatest_session with:
- - platform: "android"
- - deviceName: "Galaxy S21"
- - platformVersion: "11.0"
- - app: "lt://APP_ID_FROM_UPLOAD"
- ```
+This example demonstrates a complete e-commerce checkout flow that can be automated using MCP Appium's intelligent locator generation and test creation capabilities.
## 🙌 Contributing
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 0000000..a0d2cd0
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,392 @@
+# Contributing to MCP Appium
+
+Welcome! This guide will help you extend MCP Appium by adding new tools and resources.
+
+## Table of Contents
+
+- [Adding New Tools](#adding-new-tools)
+- [Adding New Resources](#adding-new-resources)
+- [Code Style Guidelines](#code-style-guidelines)
+- [Formatting Best Practices](#formatting-best-practices)
+
+---
+
+## Adding New Tools
+
+Tools are the core capabilities of MCP Appium. They define actions that can be performed on mobile devices.
+
+### Quick Start: Simple Tool
+
+Here's a minimal example of adding a new tool:
+
+```typescript
+// src/tools/my-new-tool.ts
+import { FastMCP } from 'fastmcp/dist/FastMCP.js';
+import { z } from 'zod';
+import { getDriver } from './sessionStore.js';
+
+export default function myNewTool(server: FastMCP): void {
+ server.addTool({
+ name: 'appium_my_new_tool',
+ description: 'Description of what this tool does',
+ parameters: z.object({
+ param1: z.string().describe('Description of param1'),
+ param2: z.number().optional().describe('Description of param2'),
+ }),
+ annotations: {
+ readOnlyHint: false, // Set to true if tool only reads data
+ openWorldHint: false, // Set to true if tool requires real-world knowledge
+ },
+ execute: async (args: any, context: any): Promise => {
+ const driver = getDriver();
+ if (!driver) {
+ throw new Error(
+ 'No active driver session. Please create a session first.'
+ );
+ }
+
+ // Your tool logic here
+ const result = await driver.someMethod(args.param1);
+
+ return {
+ content: [
+ {
+ type: 'text',
+ text: `Success message: ${result}`,
+ },
+ ],
+ };
+ },
+ });
+}
+```
+
+### Registering the Tool
+
+Add your tool to `src/tools/index.ts`:
+
+```typescript
+import myNewTool from './my-new-tool.js';
+
+export default function registerTools(server: FastMCP): void {
+ // ... existing code ...
+
+ myNewTool(server); // Add this line
+
+ // ... rest of the tools ...
+}
+```
+
+### Tool Parameters
+
+Use Zod schemas to define parameters:
+
+```typescript
+import { z } from 'zod';
+
+parameters: z.object({
+ // Required string parameter
+ requiredString: z.string().describe('A required string parameter'),
+
+ // Optional number parameter
+ optionalNumber: z.number().optional().describe('An optional number'),
+
+ // Enum parameter
+ platform: z.enum(['ios', 'android']).describe('Target platform'),
+
+ // Object parameter
+ config: z
+ .object({
+ key: z.string(),
+ value: z.string(),
+ })
+ .optional()
+ .describe('Configuration object'),
+
+ // Array parameter
+ items: z.array(z.string()).describe('List of items'),
+});
+```
+
+### Tool Annotations
+
+Annotations help the AI understand when to use your tool:
+
+- `readOnlyHint: true` - Use when the tool only retrieves/reads data without modifying state
+- `readOnlyHint: false` - Use when the tool performs actions or modifications
+- `openWorldHint: true` - Use when the tool requires knowledge beyond the codebase
+- `openWorldHint: false` - Use for codebase-specific operations
+
+### Common Patterns
+
+#### 1. Session Management Tools
+
+```typescript
+import {
+ getDriver,
+ hasActiveSession,
+ safeDeleteSession,
+} from './sessionStore.js';
+
+// Check for active session
+if (!hasActiveSession()) {
+ throw new Error('No active session. Please create a session first.');
+}
+
+const driver = getDriver();
+// Use driver...
+```
+
+#### 2. Element Interaction Tools
+
+```typescript
+import { checkIsValidElementId } from '../../utils.js';
+import { elementUUIDScheme } from '../../schema.js';
+
+// In parameters:
+parameters: z.object({
+ elementUUID: elementUUIDScheme,
+});
+
+// In execute:
+checkIsValidElementId(args.elementUUID);
+await driver.click(args.elementUUID);
+```
+
+#### 3. Platform-Specific Tools
+
+```typescript
+import { getPlatformName } from './sessionStore.js';
+
+const platform = getPlatformName(driver);
+if (platform === 'Android') {
+ // Android-specific implementation
+} else if (platform === 'iOS') {
+ // iOS-specific implementation
+}
+```
+
+---
+
+## Adding New Resources
+
+Resources provide contextual information to help the AI assist users better.
+
+### Creating a Resource
+
+```typescript
+// src/resources/my-resource.ts
+export default function myResource(server: any): void {
+ server.addResource({
+ uri: 'my://resource-uri',
+ name: 'My Resource Name',
+ description: 'Description of what this resource provides',
+ mimeType: 'text/plain', // or 'application/json', 'text/markdown', etc.
+ async load() {
+ // Return the resource content
+ return {
+ text: 'Resource content here',
+ // or
+ // data: someJSONData,
+ };
+ },
+ });
+}
+```
+
+### Registering a Resource
+
+Add your resource to `src/resources/index.ts`:
+
+```typescript
+import myResource from './my-resource.js';
+
+export default function registerResources(server: any) {
+ myResource(server); // Add this line
+ console.log('All resources registered');
+}
+```
+
+### Resource Types
+
+#### Text Resource
+
+```typescript
+{
+ uri: 'doc://example',
+ name: 'Example Resource',
+ mimeType: 'text/plain',
+ async load() {
+ return { text: 'Simple text content' };
+ }
+}
+```
+
+#### JSON Resource
+
+```typescript
+{
+ uri: 'data://example',
+ name: 'Example Data',
+ mimeType: 'application/json',
+ async load() {
+ return { data: { key: 'value' } };
+ }
+}
+```
+
+#### Markdown Resource
+
+```typescript
+{
+ uri: 'doc://guide',
+ name: 'Guide',
+ mimeType: 'text/markdown',
+ async load() {
+ return { text: '# Markdown Content' };
+ }
+}
+```
+
+---
+
+
+## Code Style Guidelines
+
+### 1. File Naming
+
+- Tools: `kebab-case.ts` (e.g., `boot-simulator.ts`)
+- Resources: `kebab-case.ts` (e.g., `java-template.ts`)
+
+### 2. Function Exports
+
+Always export as default function:
+
+```typescript
+export default function myTool(server: FastMCP): void {
+ // implementation
+}
+```
+
+### 3. Error Handling
+
+Always provide helpful error messages:
+
+```typescript
+if (!driver) {
+ throw new Error('No active driver session. Please create a session first.');
+}
+```
+
+### 4. Return Values
+
+Always return content in the expected format:
+
+```typescript
+return {
+ content: [
+ {
+ type: 'text',
+ text: 'Success message or data',
+ },
+ ],
+};
+```
+
+### 5. Async/Await
+
+Always use async/await for async operations:
+
+```typescript
+// Good
+const result = await driver.someMethod();
+
+// Bad
+driver.someMethod().then(result => ...)
+```
+
+### 6. Type Safety
+
+Use proper TypeScript types:
+
+```typescript
+execute: async (args: any, context: any): Promise => {
+ // Type your variables
+ const driver = getDriver();
+ if (!driver) {
+ throw new Error('No driver');
+ }
+ // ...
+};
+```
+
+---
+
+## Examples
+
+See these existing tools for reference:
+
+- **Simple tool**: `src/tools/scroll.ts` - Basic scrolling functionality
+- **Complex tool**: `src/tools/create-session.ts` - Session management with multiple capabilities
+- **Interaction tool**: `src/tools/interactions/click.ts` - Element interaction
+- **Prompt-based tool**: `src/tools/generate-tests.ts` - AI instructions
+
+---
+
+## Testing
+
+After adding a new tool:
+
+1. Build the project: `npm run build`
+2. Run linter: `npm run lint`
+3. Test the tool with an MCP client
+4. Verify the tool appears in the tools list
+
+---
+
+---
+
+## Formatting Best Practices
+
+### Long Descriptions
+
+For better readability when descriptions are long, use template literals with proper indentation:
+
+**Bad (hard to read):**
+```typescript
+description: 'REQUIRED: First ASK THE USER which mobile platform they want to use (Android or iOS) before creating a session. DO NOT assume or default to any platform. You MUST explicitly prompt the user to choose between Android or iOS. This is mandatory before proceeding to use the create_session tool.',
+```
+
+**Good (readable):**
+```typescript
+description: `REQUIRED: First ASK THE USER which mobile platform they want to use (Android or iOS) before creating a session.
+ DO NOT assume or default to any platform.
+ You MUST explicitly prompt the user to choose between Android or iOS.
+ This is mandatory before proceeding to use the create_session tool.
+ `,
+```
+
+### Parameter Descriptions
+
+For long parameter descriptions, also use template literals:
+
+```typescript
+parameters: z.object({
+ platform: z
+ .enum(['ios', 'android'])
+ .describe(
+ `REQUIRED: Must match the platform the user explicitly selected via the select_platform tool.
+ DO NOT default to Android or iOS without asking the user first.`
+ ),
+})
+```
+
+---
+
+## Need Help?
+
+- Check existing tools in `src/tools/`
+- See examples in `examples/`
+- Open an issue for questions
+
+Happy contributing! 🎉
diff --git a/lambdatest-capabilities.json b/lambdatest-capabilities.json
deleted file mode 100644
index 8c246b0..0000000
--- a/lambdatest-capabilities.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "android": {
- "appium:deviceName": "Galaxy S25 Ultra",
- "appium:platformVersion": "15.0",
- "appium:automationName": "UiAutomator2",
- "lt:options": {
- "app": "lt://APP_ID",
- "build": "Jarvis Appium Build",
- "name": "Android Test",
- "devicelog": true,
- "visual": true,
- "video": true,
- "autoAcceptAlerts": true,
- "autoGrantPermissions": true,
- "timezone": "UTC"
- }
- }
-}
diff --git a/package-lock.json b/package-lock.json
index 8a7c70d..9ee796d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,18 @@
{
- "name": "jarvis-appium",
- "version": "1.1.0",
+ "name": "mcp-appium",
+ "version": "1.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "jarvis-appium",
- "version": "1.1.0",
+ "name": "mcp-appium",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@langfuse/otel": "^4.2.0",
"@langfuse/tracing": "^4.2.0",
"@modelcontextprotocol/sdk": "^1.11.0",
"@opentelemetry/sdk-node": "^0.206.0",
- "@supabase/supabase-js": "^2.45.4",
"@xenova/transformers": "^2.17.2",
"@xmldom/xmldom": "^0.9.8",
"appium-adb": "^12.12.1",
@@ -22,7 +21,6 @@
"appium-xcuitest-driver": "^10.2.1",
"fast-xml-parser": "^5.2.3",
"fastmcp": "^1.23.2",
- "form-data": "^4.0.3",
"langchain": "^0.3.27",
"lodash": "^4.17.21",
"node-simctl": "^8.0.4",
@@ -31,7 +29,7 @@
"zod": "^3.24.3"
},
"bin": {
- "jarvis-appium": "dist/index.js"
+ "mcp-appium": "dist/index.js"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
@@ -41,7 +39,6 @@
"@semantic-release/github": "^10.0.2",
"@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.1",
- "@types/form-data": "^2.2.1",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.17",
"@types/node": "^22.15.18",
@@ -6315,80 +6312,6 @@
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"license": "MIT"
},
- "node_modules/@supabase/auth-js": {
- "version": "2.75.1",
- "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.75.1.tgz",
- "integrity": "sha512-zktlxtXstQuVys/egDpVsargD9hQtG20CMdtn+mMn7d2Ulkzy2tgUT5FUtpppvCJtd9CkhPHO/73rvi5W6Am5A==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "2.6.15"
- }
- },
- "node_modules/@supabase/functions-js": {
- "version": "2.75.1",
- "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.75.1.tgz",
- "integrity": "sha512-xO+01SUcwVmmo67J7Htxq8FmhkYLFdWkxfR/taxBOI36wACEUNQZmroXGPl4PkpYxBO7TaDsRHYGxUpv9zTKkg==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "2.6.15"
- }
- },
- "node_modules/@supabase/node-fetch": {
- "version": "2.6.15",
- "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
- "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
- "license": "MIT",
- "dependencies": {
- "whatwg-url": "^5.0.0"
- },
- "engines": {
- "node": "4.x || >=6.0.0"
- }
- },
- "node_modules/@supabase/postgrest-js": {
- "version": "2.75.1",
- "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.75.1.tgz",
- "integrity": "sha512-FiYBD0MaKqGW8eo4Xqu7/100Xm3ddgh+3qHtqS18yQRoglJTFRQCJzY1xkrGS0JFHE2YnbjL6XCiOBXiG8DK4Q==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "2.6.15"
- }
- },
- "node_modules/@supabase/realtime-js": {
- "version": "2.75.1",
- "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.75.1.tgz",
- "integrity": "sha512-lBIJ855bUsBFScHA/AY+lxIFkubduUvmwbagbP1hq0wDBNAsYdg3ql80w8YmtXCDjkCwlE96SZqcFn7BGKKJKQ==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "2.6.15",
- "@types/phoenix": "^1.6.6",
- "@types/ws": "^8.18.1",
- "ws": "^8.18.2"
- }
- },
- "node_modules/@supabase/storage-js": {
- "version": "2.75.1",
- "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.75.1.tgz",
- "integrity": "sha512-WdGEhroflt5O398Yg3dpf1uKZZ6N3CGloY9iGsdT873uWbkQKoP0wG8mtx98dh0fhj6dAlzBqOAvnlV12cJfzA==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "2.6.15"
- }
- },
- "node_modules/@supabase/supabase-js": {
- "version": "2.75.1",
- "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.75.1.tgz",
- "integrity": "sha512-GEPVBvjQimcMd9z5K1eTKTixTRb6oVbudoLQ9JKqTUJnR6GQdBU4OifFZean1AnHfsQwtri1fop2OWwsMv019w==",
- "license": "MIT",
- "dependencies": {
- "@supabase/auth-js": "2.75.1",
- "@supabase/functions-js": "2.75.1",
- "@supabase/node-fetch": "2.6.15",
- "@supabase/postgrest-js": "2.75.1",
- "@supabase/realtime-js": "2.75.1",
- "@supabase/storage-js": "2.75.1"
- }
- },
"node_modules/@tokenizer/inflate": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
@@ -6498,16 +6421,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/form-data": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz",
- "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -6601,12 +6514,6 @@
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
"license": "MIT"
},
- "node_modules/@types/phoenix": {
- "version": "1.6.6",
- "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
- "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
- "license": "MIT"
- },
"node_modules/@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
@@ -6633,15 +6540,6 @@
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
- "node_modules/@types/ws": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
- "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@@ -31427,6 +31325,7 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=10.0.0"
},
diff --git a/package.json b/package.json
index 11ac713..fbadbeb 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,14 @@
{
- "name": "jarvis-appium",
+ "name": "mcp-appium",
"version": "1.1.1",
"type": "module",
"main": "src/index.ts",
"bin": {
- "jarvis-appium": "dist/index.js"
+ "mcp-appium": "dist/index.js"
},
"scripts": {
"build": "rimraf dist && tsc && chmod +x dist/index.js && npm run copy-docs",
- "copy-docs": "mkdir -p dist/tools/documentation/uploads && cp -f src/tools/documentation/uploads/documents.json dist/tools/documentation/uploads/documents.json 2>/dev/null || echo 'No documents.json found, run npm run index-docs first'",
+ "copy-docs": "mkdir -p dist/tools/documentation/uploads && cp -f src/tools/documentation/uploads/documents.json dist/tools/documentation/uploads/documents.json 2>/dev/null || true",
"start": "node src/index.js",
"start:stdio": "node src/index.js",
"start:sse": "node src/index.js --sse",
@@ -34,7 +34,6 @@
"@langfuse/tracing": "^4.2.0",
"@modelcontextprotocol/sdk": "^1.11.0",
"@opentelemetry/sdk-node": "^0.206.0",
- "@supabase/supabase-js": "^2.45.4",
"@xenova/transformers": "^2.17.2",
"@xmldom/xmldom": "^0.9.8",
"appium-adb": "^12.12.1",
@@ -43,7 +42,6 @@
"appium-xcuitest-driver": "^10.2.1",
"fast-xml-parser": "^5.2.3",
"fastmcp": "^1.23.2",
- "form-data": "^4.0.3",
"langchain": "^0.3.27",
"lodash": "^4.17.21",
"node-simctl": "^8.0.4",
@@ -60,7 +58,6 @@
"@semantic-release/github": "^10.0.2",
"conventional-changelog-conventionalcommits": "^8.0.0",
"@jest/globals": "^29.7.0",
- "@types/form-data": "^2.2.1",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.17",
"@types/node": "^22.15.18",
diff --git a/src/index.ts b/src/index.ts
index 75f1ec0..6d001cd 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -11,7 +11,7 @@ const port =
// Start the server with the appropriate transport
async function startServer(): Promise {
- log.info('Starting Jarvis Appium MCP Server...');
+ log.info('Starting MCP Appium MCP Server...');
try {
if (useSSE) {
diff --git a/src/scripts/simple-index-documentation.ts b/src/scripts/simple-index-documentation.ts
index d9d51f1..a446b72 100644
--- a/src/scripts/simple-index-documentation.ts
+++ b/src/scripts/simple-index-documentation.ts
@@ -47,9 +47,7 @@ if (args.length > 0 && args[0]) {
}
} else {
// Use default path to resources directory
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = path.dirname(__filename);
- markdownPath = path.resolve(__dirname, '../resources');
+ markdownPath = path.resolve(process.cwd(), 'src/resources');
console.log(`Using default resources directory: ${markdownPath}`);
}
diff --git a/src/server.ts b/src/server.ts
index 3db2715..0391586 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,42 +1,16 @@
import { FastMCP } from 'fastmcp';
-import { createClient } from '@supabase/supabase-js';
-import { TraceMiddleware, SupabaseTraceAdapter } from './utils/tracing.js';
import registerTools from './tools/index.js';
import registerResources from './resources/index.js';
import { hasActiveSession, safeDeleteSession } from './tools/sessionStore.js';
import { log } from './locators/logger.js';
const server = new FastMCP({
- name: 'Jarvis Appium',
+ name: 'MCP Appium',
version: '1.0.0',
instructions:
'Intelligent MCP server providing AI assistants with powerful tools and resources for Appium mobile automation',
});
-// Initialize Supabase tracing if environment variables are provided
-const SUPABASE_URL = process.env.SUPABASE_URL;
-const SUPABASE_KEY = process.env.SUPABASE_KEY;
-
-if (SUPABASE_URL && SUPABASE_KEY) {
- try {
- const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
- const supabaseAdapter = new SupabaseTraceAdapter({
- supabaseClient: supabase,
- });
- const traceMiddleware = new TraceMiddleware({ adapter: supabaseAdapter });
-
- // Initialize tracing BEFORE registering tools so it can wrap them
- traceMiddleware.init(server);
- log.info('Supabase tracing middleware initialized successfully');
- } catch (error) {
- log.error('Failed to initialize Supabase tracing middleware:', error);
- }
-} else {
- log.info(
- 'Supabase tracing disabled - SUPABASE_URL and SUPABASE_KEY environment variables not provided'
- );
-}
-
registerResources(server);
registerTools(server);
diff --git a/src/tools/README.md b/src/tools/README.md
new file mode 100644
index 0000000..b181f0f
--- /dev/null
+++ b/src/tools/README.md
@@ -0,0 +1,186 @@
+# Tools Directory
+
+This directory contains all MCP tools available in MCP Appium.
+
+## Tool Categories
+
+### Session Management
+
+- `create-session.ts` - Create mobile automation sessions
+- `delete-session.ts` - Clean up sessions
+- `select-platform.ts` - Choose Android or iOS
+- `select-device.ts` - Choose specific device
+
+### iOS Setup
+
+- `boot-simulator.ts` - Boot iOS simulators
+- `setup-wda.ts` - Setup WebDriverAgent
+- `install-wda.ts` - Install WebDriverAgent
+
+### Element Interaction
+
+- `interactions/` - Direct appium interactions
+ - `find.ts` - Find elements
+ - `click.ts` - Click elements
+ - `doubleTap.ts` - Double tap elements
+ - `setValue.ts` - Enter text
+ - `getText.ts` - Get element text
+ - `screenshot.ts` - Capture screenshots
+ - `activateApp.ts` - Activate apps
+ - `terminateApp.ts` - Terminate apps
+ - `installApp.ts` - Install apps
+ - `uninstallApp.ts` - Uninstall apps
+ - `listApps.ts` - List installed apps
+
+### Navigation
+
+- `scroll.ts` - Scroll screens
+- `scroll-to-element.ts` - Scroll until element found
+
+### AI & Test Generation
+
+- `generate_locators.ts` - Generate page locators
+- `generate-tests.ts` - Generate test code from scenarios
+- `answerAppium.ts` - Answer Appium questions
+
+## Adding a New Tool
+
+See [docs/CONTRIBUTING.md](../../docs/CONTRIBUTING.md) for detailed instructions.
+
+Quick steps:
+
+1. Create a new file in this directory (e.g., `my-tool.ts`)
+2. Define the tool following the template
+3. Register it in `index.ts`
+4. Test with `npm run build && npm start`
+
+### Tool Template
+
+```typescript
+import { FastMCP } from 'fastmcp/dist/FastMCP.js';
+import { z } from 'zod';
+import { getDriver } from './sessionStore.js';
+
+export default function myTool(server: FastMCP): void {
+ server.addTool({
+ name: 'appium_my_tool',
+ description: 'What this tool does',
+ parameters: z.object({
+ param: z.string().describe('Parameter description'),
+ }),
+ annotations: {
+ readOnlyHint: false,
+ openWorldHint: false,
+ },
+ execute: async (args: any, context: any): Promise => {
+ const driver = getDriver();
+ if (!driver) {
+ throw new Error('No active session');
+ }
+
+ // Implementation
+
+ return {
+ content: [
+ {
+ type: 'text',
+ text: 'Success',
+ },
+ ],
+ };
+ },
+ });
+}
+```
+
+### Registering a Tool
+
+Add to `src/tools/index.ts`:
+
+```typescript
+import myTool from './my-tool.js';
+
+export default function registerTools(server: FastMCP): void {
+ // ... existing tools ...
+ myTool(server);
+ // ...
+}
+```
+
+## Best Practices
+
+1. **Always check for active session**: Use `getDriver()` and check for null
+2. **Provide helpful errors**: Give clear error messages
+3. **Use proper types**: Leverage TypeScript and Zod for type safety
+4. **Add logging**: Use `console.log` for debugging
+5. **Handle errors**: Always wrap risky operations in try-catch
+6. **Return proper format**: Always return content in expected MCP format
+
+## Session Store
+
+Tools interact with the session through `sessionStore.ts`:
+
+```typescript
+import {
+ getDriver,
+ hasActiveSession,
+ safeDeleteSession,
+} from './sessionStore.js';
+
+// Check if session exists
+if (!hasActiveSession()) {
+ throw new Error('No active session');
+}
+
+// Get the driver
+const driver = getDriver();
+
+// Use the driver
+await driver.someMethod();
+```
+
+## Common Patterns
+
+### Platform-Specific Logic
+
+```typescript
+import { getPlatformName } from './sessionStore.js';
+
+if (getPlatformName(driver) === 'Android') {
+ // Android implementation
+} else if (getPlatformName(driver) === 'iOS') {
+ // iOS implementation
+}
+```
+
+### Element Operations
+
+```typescript
+import { elementUUIDScheme } from '../schema.js';
+import { checkIsValidElementId } from '../utils.js';
+
+// In parameters
+parameters: z.object({
+ elementUUID: elementUUIDScheme,
+});
+
+// In execute
+checkIsValidElementId(args.elementUUID);
+await driver.click(args.elementUUID);
+```
+
+## Testing
+
+After adding a new tool:
+
+1. Build: `npm run build`
+2. Run linter: `npm run lint`
+3. Test with an MCP client
+4. Verify tool appears in tools list
+
+## Need Help?
+
+- Check existing tools for examples
+- Read [docs/CONTRIBUTING.md](../../docs/CONTRIBUTING.md)
+- Look at examples in `examples/` directory
+- Open an issue for questions
diff --git a/src/tools/answerAppium.ts b/src/tools/answerAppium.ts
index 8d7bde9..9a2287f 100644
--- a/src/tools/answerAppium.ts
+++ b/src/tools/answerAppium.ts
@@ -10,7 +10,9 @@ import {
export default function answerAppium(server: any): void {
server.addTool({
name: 'appium_documentation_query',
- description: `Query Appium documentation using RAG (Retrieval-Augmented Generation). This tool searches through indexed Appium documentation to answer questions about Appium features, setup, configuration, drivers, and usage.`,
+ description: `Query Appium documentation using RAG (Retrieval-Augmented Generation).
+ This tool searches through indexed Appium documentation to answer questions about Appium features, setup, configuration, drivers, and usage.
+ `,
parameters: z.object({
query: z
.string()
diff --git a/src/tools/boot-simulator.ts b/src/tools/boot-simulator.ts
index 23406d7..79fb03c 100644
--- a/src/tools/boot-simulator.ts
+++ b/src/tools/boot-simulator.ts
@@ -8,14 +8,13 @@ import { IOSManager } from '../devicemanager/ios-manager.js';
export default function bootSimulator(server: any): void {
server.addTool({
name: 'boot_simulator',
- description:
- 'Boot an iOS simulator and wait for it to be ready. This speeds up subsequent session creation by ensuring the simulator is already running.',
+ description: `Boot an iOS simulator and wait for it to be ready.
+ This speeds up subsequent session creation by ensuring the simulator is already running.`,
parameters: z.object({
- udid: z
- .string()
- .describe(
- 'The UDID of the iOS simulator to boot. Use select_platform and select_device tools first to get the UDID.'
- ),
+ udid: z.string().describe(
+ `The UDID of the iOS simulator to boot.
+ Use select_platform and select_device tools first to get the UDID.`
+ ),
}),
annotations: {
readOnlyHint: false,
diff --git a/src/tools/create-cloud-session.ts b/src/tools/create-cloud-session.ts
deleted file mode 100644
index ec13071..0000000
--- a/src/tools/create-cloud-session.ts
+++ /dev/null
@@ -1,446 +0,0 @@
-/**
- * Tool to create a new mobile session on LambdaTest cloud platform
- */
-import { z } from 'zod';
-import { setSession } from './sessionStore.js';
-
-// Define LambdaTest capabilities type
-interface LambdaTestCapabilities {
- platformName: string;
- 'appium:automationName': string;
- 'appium:deviceName': string;
- 'appium:platformVersion': string;
- 'lt:options': {
- username: string;
- accessKey: string;
- build: string;
- name: string;
- devicelog: boolean;
- visual: boolean;
- video: boolean;
- autoAcceptAlerts?: boolean;
- autoGrantPermissions?: boolean;
- [key: string]: any;
- };
- [key: string]: any;
-}
-
-// Simple WebDriver client for LambdaTest
-class LambdaTestWebDriver {
- private baseUrl: string;
- private authHeader: string;
- private sessionId: string | null = null;
-
- constructor(username: string, accessKey: string) {
- this.baseUrl = 'https://mobile-hub.lambdatest.com/wd/hub';
- this.authHeader = `Basic ${Buffer.from(`${username}:${accessKey}`).toString('base64')}`;
- }
-
- async createSession(capabilities: LambdaTestCapabilities): Promise {
- const response = await fetch(`${this.baseUrl}/session`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: this.authHeader,
- },
- body: JSON.stringify({
- capabilities: {
- alwaysMatch: capabilities,
- firstMatch: [{}],
- },
- }),
- });
-
- if (!response.ok) {
- const errorText = await response.text();
- throw new Error(
- `Failed to create session: ${response.status} ${errorText}`
- );
- }
-
- const result = await response.json();
- this.sessionId = result.value.sessionId;
- if (!this.sessionId) {
- throw new Error('Failed to get session ID from response');
- }
- return this.sessionId;
- }
-
- async deleteSession(): Promise {
- if (!this.sessionId) return;
-
- await fetch(`${this.baseUrl}/session/${this.sessionId}`, {
- method: 'DELETE',
- headers: {
- Authorization: this.authHeader,
- },
- });
- this.sessionId = null;
- }
-
- async getPageSource(): Promise {
- if (!this.sessionId) throw new Error('No active session');
-
- const response = await fetch(
- `${this.baseUrl}/session/${this.sessionId}/source`,
- {
- method: 'GET',
- headers: {
- Authorization: this.authHeader,
- },
- }
- );
-
- if (!response.ok) {
- throw new Error(`Failed to get page source: ${response.status}`);
- }
-
- const result = await response.json();
- return result.value;
- }
-
- async findElement(strategy: string, selector: string): Promise {
- if (!this.sessionId) throw new Error('No active session');
-
- const response = await fetch(
- `${this.baseUrl}/session/${this.sessionId}/element`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: this.authHeader,
- },
- body: JSON.stringify({
- using: strategy,
- value: selector,
- }),
- }
- );
-
- if (!response.ok) {
- throw new Error(`Failed to find element: ${response.status}`);
- }
-
- const result = await response.json();
- return (
- result.value.ELEMENT ||
- result.value['element-6066-11e4-a52e-4f735466cecf']
- );
- }
-
- async click(elementId: string): Promise {
- if (!this.sessionId) throw new Error('No active session');
-
- const response = await fetch(
- `${this.baseUrl}/session/${this.sessionId}/element/${elementId}/click`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: this.authHeader,
- },
- body: JSON.stringify({}),
- }
- );
-
- if (!response.ok) {
- throw new Error(`Failed to click element: ${response.status}`);
- }
- }
-
- async sendKeys(elementId: string, text: string): Promise {
- if (!this.sessionId) throw new Error('No active session');
-
- const response = await fetch(
- `${this.baseUrl}/session/${this.sessionId}/element/${elementId}/value`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: this.authHeader,
- },
- body: JSON.stringify({
- text: text,
- }),
- }
- );
-
- if (!response.ok) {
- throw new Error(`Failed to send keys: ${response.status}`);
- }
- }
-
- async getText(elementId: string): Promise {
- if (!this.sessionId) throw new Error('No active session');
-
- const response = await fetch(
- `${this.baseUrl}/session/${this.sessionId}/element/${elementId}/text`,
- {
- method: 'GET',
- headers: {
- Authorization: this.authHeader,
- },
- }
- );
-
- if (!response.ok) {
- throw new Error(`Failed to get text: ${response.status}`);
- }
-
- const result = await response.json();
- return result.value;
- }
-
- async takeScreenshot(): Promise {
- if (!this.sessionId) throw new Error('No active session');
-
- const response = await fetch(
- `${this.baseUrl}/session/${this.sessionId}/screenshot`,
- {
- method: 'GET',
- headers: {
- Authorization: this.authHeader,
- },
- }
- );
-
- if (!response.ok) {
- throw new Error(`Failed to take screenshot: ${response.status}`);
- }
-
- const result = await response.json();
- return result.value;
- }
-
- getSessionId(): string | null {
- return this.sessionId;
- }
-
- // Add compatibility methods for existing tools
- get caps() {
- return {
- automationName: 'UiAutomator2', // or XCUITest for iOS
- };
- }
-}
-
-export default function createCloudSession(server: any): void {
- server.addTool({
- name: 'create_lambdatest_session',
- description:
- 'Create a new mobile session on LambdaTest cloud platform with real devices or simulators',
- parameters: z.object({
- platform: z
- .enum(['ios', 'android'])
- .describe(
- "Platform - 'android' for Android devices or 'ios' for iOS devices"
- ),
- deviceName: z
- .string()
- .describe(
- 'Device name (e.g., "Galaxy S21", "iPhone 13 Pro", "Pixel 6")'
- ),
- platformVersion: z
- .string()
- .describe('Platform version (e.g., "11.0", "15.0", "12")'),
- app: z
- .string()
- .optional()
- .describe('App URL or app ID from LambdaTest app upload (lt://APP_ID)'),
- buildName: z
- .string()
- .optional()
- .describe(
- 'Build name for organizing test runs (default: "Jarvis Appium Build")'
- ),
- testName: z
- .string()
- .optional()
- .describe('Test name for this session (default: "Jarvis Appium Test")'),
- capabilities: z
- .object({})
- .optional()
- .describe('Additional custom capabilities for the session'),
- ltOptions: z
- .object({
- devicelog: z
- .boolean()
- .optional()
- .describe('Enable device logs (default: true)'),
- visual: z
- .boolean()
- .optional()
- .describe('Enable visual testing (default: true)'),
- video: z
- .boolean()
- .optional()
- .describe('Enable video recording (default: true)'),
- autoAcceptAlerts: z
- .boolean()
- .optional()
- .describe('Auto accept alerts (default: true)'),
- autoGrantPermissions: z
- .boolean()
- .optional()
- .describe('Auto grant permissions (default: true)'),
- timezone: z
- .string()
- .optional()
- .describe('Device timezone (e.g., "UTC", "America/New_York")'),
- location: z
- .string()
- .optional()
- .describe('Device location (e.g., "US", "IN", "GB")'),
- })
- .optional()
- .describe('LambdaTest specific options'),
- }),
- annotations: {
- readOnlyHint: false,
- openWorldHint: false,
- },
- execute: async (args: any, context: any): Promise => {
- try {
- const {
- platform,
- deviceName,
- platformVersion,
- app,
- buildName = 'Jarvis Appium Build',
- testName = 'Jarvis Appium Test',
- capabilities: customCapabilities = {},
- ltOptions = {},
- } = args;
-
- // Get LambdaTest credentials from environment or use provided defaults
- const ltUsername = process.env.LT_USERNAME;
- const ltAccessKey = process.env.LT_ACCESS_KEY;
-
- let finalCapabilities: LambdaTestCapabilities;
-
- // LambdaTest hub URL
- const hubUrl = `https://${ltUsername}:${ltAccessKey}@mobile-hub.lambdatest.com/wd/hub`;
-
- if (platform === 'android') {
- finalCapabilities = {
- platformName: 'Android',
- 'appium:automationName': 'UiAutomator2',
- 'appium:deviceName': deviceName,
- 'appium:platformVersion': platformVersion,
- 'lt:options': {
- username: ltUsername,
- accessKey: ltAccessKey,
- build: buildName,
- name: testName,
- isRealMobile: true,
- devicelog: ltOptions.devicelog ?? true,
- visual: ltOptions.visual ?? true,
- video: ltOptions.video ?? true,
- autoAcceptAlerts: ltOptions.autoAcceptAlerts ?? true,
- autoGrantPermissions: ltOptions.autoGrantPermissions ?? true,
- ...ltOptions,
- },
- ...customCapabilities,
- };
-
- if (app) {
- finalCapabilities['lt:options'].app = app;
- }
- } else if (platform === 'ios') {
- finalCapabilities = {
- platformName: 'iOS',
- 'appium:automationName': 'XCUITest',
- 'appium:deviceName': deviceName,
- 'appium:platformVersion': platformVersion,
- 'lt:options': {
- username: ltUsername,
- accessKey: ltAccessKey,
- build: buildName,
- name: testName,
- isRealMobile: true,
- devicelog: ltOptions.devicelog ?? true,
- visual: ltOptions.visual ?? true,
- video: ltOptions.video ?? true,
- autoAcceptAlerts: ltOptions.autoAcceptAlerts ?? true,
- autoGrantPermissions: ltOptions.autoGrantPermissions ?? true,
- ...ltOptions,
- },
- ...customCapabilities,
- };
-
- if (app) {
- finalCapabilities['lt:options'].app = app;
- }
- } else {
- throw new Error(
- `Unsupported platform: ${platform}. Please choose 'android' or 'ios'.`
- );
- }
-
- console.log('Connecting to LambdaTest cloud platform...');
- console.log(
- `Creating new ${platform.toUpperCase()} session on LambdaTest cloud with capabilities:`,
- JSON.stringify(finalCapabilities, null, 2)
- );
-
- // Create WebDriver client and session
- const driver = new LambdaTestWebDriver(ltUsername!, ltAccessKey!);
- const sessionId = await driver.createSession(finalCapabilities);
-
- // Store the session
- setSession(driver, sessionId);
-
- console.log(
- `${platform.toUpperCase()} session created successfully on LambdaTest with ID: ${sessionId}`
- );
-
- const sessionUrl = `https://automation.lambdatest.com/build/${buildName}`;
-
- return {
- content: [
- {
- type: 'text',
- text: `${platform.toUpperCase()} session created successfully on LambdaTest Cloud!
-
-Session Details:
-- Session ID: ${sessionId}
-- Platform: ${finalCapabilities.platformName}
-- Device: ${finalCapabilities['appium:deviceName']}
-- Platform Version: ${finalCapabilities['appium:platformVersion']}
-- Automation: ${finalCapabilities['appium:automationName']}
-- Build: ${buildName}
-- Test Name: ${testName}
-
-View your test execution at: ${sessionUrl}
-
-LambdaTest Features Enabled:
-- Device Logs: ${finalCapabilities['lt:options'].devicelog}
-- Visual Testing: ${finalCapabilities['lt:options'].visual}
-- Video Recording: ${finalCapabilities['lt:options'].video}
-- Auto Accept Alerts: ${finalCapabilities['lt:options'].autoAcceptAlerts}
-- Auto Grant Permissions: ${finalCapabilities['lt:options'].autoGrantPermissions}`,
- },
- ],
- };
- } catch (error: any) {
- console.error('Error creating LambdaTest session:', error);
-
- // Provide helpful error messages for common issues
- let errorMessage = `Failed to create LambdaTest session: ${error.message}`;
-
- if (error.message.includes('authentication')) {
- errorMessage +=
- '\n\nTip: Check your LambdaTest username and access key. You can find them at: https://accounts.lambdatest.com/security';
- } else if (error.message.includes('device')) {
- errorMessage +=
- '\n\nTip: Verify the device name and platform version are available on LambdaTest. Check: https://www.lambdatest.com/capabilities-generator/';
- } else if (error.message.includes('app')) {
- errorMessage +=
- '\n\nTip: Make sure the app URL is correct. Upload your app first using LambdaTest app upload API.';
- }
-
- throw new Error(errorMessage);
- }
- },
- });
-}
diff --git a/src/tools/create-session.ts b/src/tools/create-session.ts
index c1ee96d..223c31e 100644
--- a/src/tools/create-session.ts
+++ b/src/tools/create-session.ts
@@ -2,7 +2,8 @@
* Tool to create a new mobile session (Android or iOS)
*/
import { z } from 'zod';
-import fs from 'fs';
+import { access, readFile } from 'fs/promises';
+import { constants } from 'fs';
import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver';
import { XCUITestDriver } from 'appium-xcuitest-driver';
import {
@@ -35,14 +36,15 @@ interface CapabilitiesConfig {
export default function createSession(server: any): void {
server.addTool({
name: 'create_session',
- description:
- 'Create a new mobile session with Android or iOS device (MUST use select_platform tool first to ask the user which platform they want - DO NOT assume or default to any platform)',
+ description: `Create a new mobile session with Android or iOS device.
+ MUST use select_platform tool first to ask the user which platform they want.
+ DO NOT assume or default to any platform.
+ `,
parameters: z.object({
- platform: z
- .enum(['ios', 'android'])
- .describe(
- 'REQUIRED: Must match the platform the user explicitly selected via the select_platform tool. DO NOT default to Android or iOS without asking the user first.'
- ),
+ platform: z.enum(['ios', 'android']).describe(
+ `REQUIRED: Must match the platform the user explicitly selected via the select_platform tool.
+ DO NOT default to Android or iOS without asking the user first.`
+ ),
capabilities: z
.object({})
.optional()
@@ -72,9 +74,12 @@ export default function createSession(server: any): void {
let configCapabilities: CapabilitiesConfig = { android: {}, ios: {} };
const configPath = process.env.CAPABILITIES_CONFIG;
- if (configPath && fs.existsSync(configPath)) {
+ if (configPath) {
try {
- const configContent = fs.readFileSync(configPath, 'utf8');
+ // Check if file exists
+ await access(configPath, constants.F_OK);
+ // Read file content
+ const configContent = await readFile(configPath, 'utf8');
configCapabilities = JSON.parse(configContent);
} catch (error) {
console.warn(`Failed to parse capabilities config: ${error}`);
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 0f1cd65..3ebdad2 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -1,10 +1,22 @@
+/**
+ * Tools Registration Module
+ *
+ * This file registers all available MCP tools with the server.
+ *
+ * ADDING A NEW TOOL:
+ * 1. Create your tool file in src/tools/
+ * 2. Import it at the top of this file
+ * 3. Call it in the registerTools function below
+ *
+ * See docs/CONTRIBUTING.md for detailed instructions.
+ * See src/tools/README.md for tool organization.
+ * See src/tools/metadata/README.md for YAML metadata approach.
+ */
import { FastMCP } from 'fastmcp/dist/FastMCP.js';
import { log } from '../locators/logger.js';
import answerAppium from './answerAppium.js';
import createSession from './create-session.js';
import deleteSession from './delete-session.js';
-import createCloudSession from './create-cloud-session.js';
-import uploadApp from './upload-app.js';
import generateLocators from './locators.js';
import selectPlatform from './select-platform.js';
import selectDevice from './select-device.js';
@@ -100,8 +112,6 @@ export default function registerTools(server: FastMCP): void {
installWDA(server);
createSession(server);
deleteSession(server);
- createCloudSession(server);
- uploadApp(server);
generateLocators(server);
answerAppium(server);
scroll(server);
diff --git a/src/tools/install-wda.ts b/src/tools/install-wda.ts
index aecc9bd..5becd89 100644
--- a/src/tools/install-wda.ts
+++ b/src/tools/install-wda.ts
@@ -5,7 +5,9 @@ import { z } from 'zod';
import { exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
-import fs from 'fs';
+import { access, readdir, stat } from 'fs/promises';
+import { constants } from 'fs';
+import fs from 'fs'; // Keep for createWriteStream if used
import os from 'os';
const execAsync = promisify(exec);
@@ -17,25 +19,36 @@ function cachePath(folder: string): string {
async function getLatestWDAVersion(): Promise {
// Scan the cache directory to find the latest version
const wdaCacheDir = cachePath('wda');
- if (!fs.existsSync(wdaCacheDir)) {
+
+ try {
+ await access(wdaCacheDir, constants.F_OK);
+ } catch {
throw new Error('No WDA cache found. Please run setup_wda first.');
}
- const versions = fs
- .readdirSync(wdaCacheDir)
- .filter(dir => fs.statSync(path.join(wdaCacheDir, dir)).isDirectory())
+ const entries = await readdir(wdaCacheDir);
+ const versions = await Promise.all(
+ entries.map(async dir => {
+ const dirPath = path.join(wdaCacheDir, dir);
+ const stats = await stat(dirPath);
+ return stats.isDirectory() ? dir : null;
+ })
+ );
+
+ const filteredVersions = versions
+ .filter((v): v is string => v !== null)
.sort((a, b) => {
// Simple version comparison - you might want to use semver for more complex versions
return b.localeCompare(a, undefined, { numeric: true });
});
- if (versions.length === 0) {
+ if (filteredVersions.length === 0) {
throw new Error(
'No WDA versions found in cache. Please run setup_wda first.'
);
}
- return versions[0];
+ return filteredVersions[0];
}
async function getBootedSimulators(): Promise {
@@ -145,8 +158,9 @@ async function isWDARunning(simulatorUdid: string): Promise {
export default function installWDA(server: any): void {
server.addTool({
name: 'install_wda',
- description:
- 'Install and launch the WebDriverAgent (WDA) app on a booted iOS simulator using the app path from setup_wda. This tool requires WDA to be already set up using setup_wda and at least one simulator to be booted.',
+ description: `Install and launch the WebDriverAgent (WDA) app on a booted iOS simulator using the app path from setup_wda.
+ This tool requires WDA to be already set up using setup_wda and at least one simulator to be booted.
+ `,
parameters: z.object({
simulatorUdid: z
.string()
@@ -158,7 +172,8 @@ export default function installWDA(server: any): void {
.string()
.optional()
.describe(
- 'The path to the WDA app bundle (.app file) that be generated by setup_wda tool. If not provided, will try to find the latest cached WDA app.'
+ `The path to the WDA app bundle (.app file) that be generated by setup_wda tool.
+ If not provided, will try to find the latest cached WDA app.`
),
}),
annotations: {
@@ -188,7 +203,9 @@ export default function installWDA(server: any): void {
}
// Verify WDA app exists
- if (!fs.existsSync(appPath)) {
+ try {
+ await access(appPath, constants.F_OK);
+ } catch {
throw new Error(
`WDA app not found at ${appPath}. Please run setup_wda first to download and cache WDA, or provide a valid appPath.`
);
diff --git a/src/tools/interactions/screenshot.ts b/src/tools/interactions/screenshot.ts
index 0cadf33..a17b9ad 100644
--- a/src/tools/interactions/screenshot.ts
+++ b/src/tools/interactions/screenshot.ts
@@ -1,11 +1,14 @@
import { FastMCP } from 'fastmcp/dist/FastMCP.js';
import { z } from 'zod';
import { getDriver } from '../sessionStore.js';
+import { writeFile } from 'fs/promises';
+import { join } from 'path';
export default function screenshot(server: FastMCP): void {
server.addTool({
name: 'appium_screenshot',
- description: 'Take a screenshot of the current screen in base64 format',
+ description:
+ 'Take a screenshot of the current screen and return as PNG image',
annotations: {
readOnlyHint: false,
openWorldHint: false,
@@ -17,12 +20,24 @@ export default function screenshot(server: FastMCP): void {
}
try {
- const screenshot = await driver.getScreenshot();
+ const screenshotBase64 = await driver.getScreenshot();
+
+ // Convert base64 to buffer
+ const screenshotBuffer = Buffer.from(screenshotBase64, 'base64');
+
+ // Generate filename with timestamp
+ const timestamp = Date.now();
+ const filename = `screenshot_${timestamp}.png`;
+ const filepath = join(process.cwd(), filename);
+
+ // Save screenshot to disk
+ await writeFile(filepath, screenshotBuffer);
+
return {
content: [
{
type: 'text',
- text: screenshot,
+ text: `Screenshot saved successfully to: ${filename}`,
},
],
};
diff --git a/src/tools/locators.ts b/src/tools/locators.ts
index 87af2a0..c157a40 100644
--- a/src/tools/locators.ts
+++ b/src/tools/locators.ts
@@ -1,5 +1,13 @@
/**
* Tool to get page source from the Android session
+ *
+ * TOOL EXTENSION GUIDE:
+ * This tool demonstrates the traditional approach where metadata is defined inline.
+ *
+ * ALTERNATIVE APPROACH: You can also use YAML metadata files for better separation.
+ * See src/tools/metadata/ for examples and src/tools/scroll-with-yaml.example.ts
+ *
+ * For detailed documentation on adding tools, see docs/CONTRIBUTING.md
*/
import { z } from 'zod';
import { getDriver } from './sessionStore.js';
diff --git a/src/tools/select-platform.ts b/src/tools/select-platform.ts
index 041f5d2..5abd32b 100644
--- a/src/tools/select-platform.ts
+++ b/src/tools/select-platform.ts
@@ -9,8 +9,11 @@ import { log } from '../locators/logger.js';
export default function selectPlatform(server: any): void {
server.addTool({
name: 'select_platform',
- description:
- 'REQUIRED: First ASK THE USER which mobile platform they want to use (Android or iOS) before creating a session. DO NOT assume or default to any platform. You MUST explicitly prompt the user to choose between Android or iOS. This is mandatory before proceeding to use the create_session tool.',
+ description: `REQUIRED: First ASK THE USER which mobile platform they want to use (Android or iOS) before creating a session.
+ DO NOT assume or default to any platform.
+ You MUST explicitly prompt the user to choose between Android or iOS.
+ This is mandatory before proceeding to use the create_session tool.
+ `,
parameters: z.object({
platform: z
.enum(['ios', 'android'])
diff --git a/src/tools/setup-wda.ts b/src/tools/setup-wda.ts
index 3ae7f23..e4d6683 100644
--- a/src/tools/setup-wda.ts
+++ b/src/tools/setup-wda.ts
@@ -5,7 +5,9 @@ import { z } from 'zod';
import { exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
-import fs from 'fs';
+import { access, mkdir, unlink } from 'fs/promises';
+import { constants } from 'fs';
+import fs from 'fs'; // Keep for createWriteStream
import os from 'os';
import https from 'https';
@@ -63,7 +65,7 @@ async function downloadFile(url: string, destPath: string): Promise {
.get(url, response => {
if (response.statusCode === 302 || response.statusCode === 301) {
file.close();
- fs.unlinkSync(destPath);
+ unlink(destPath).catch(() => {});
return downloadFile(response.headers.location!, destPath)
.then(resolve)
.catch(reject);
@@ -71,7 +73,7 @@ async function downloadFile(url: string, destPath: string): Promise {
if (response.statusCode !== 200) {
file.close();
- fs.unlinkSync(destPath);
+ unlink(destPath).catch(() => {});
return reject(
new Error(`Failed to download: ${response.statusCode}`)
);
@@ -84,10 +86,13 @@ async function downloadFile(url: string, destPath: string): Promise {
resolve();
});
})
- .on('error', err => {
+ .on('error', async err => {
file.close();
- if (fs.existsSync(destPath)) {
- fs.unlinkSync(destPath);
+ try {
+ await access(destPath, constants.F_OK);
+ await unlink(destPath);
+ } catch {
+ // File doesn't exist or already deleted
}
reject(err);
});
@@ -101,15 +106,20 @@ async function unzipFile(zipPath: string, destDir: string): Promise {
export default function setupWDA(server: any): void {
server.addTool({
name: 'setup_wda',
- description:
- 'Download and setup prebuilt WebDriverAgent (WDA) for iOS/tvOS simulators only (not for real devices). This significantly speeds up the first Appium session by avoiding the need to build WDA from source. Downloads the latest version from GitHub and caches it locally.',
+ description: `Download and setup prebuilt WebDriverAgent (WDA) for iOS/tvOS simulators only (not for real devices).
+ This significantly speeds up the first Appium session by avoiding the need to build WDA from source.
+ Downloads the latest version from GitHub and caches it locally.
+ `,
parameters: z.object({
platform: z
.enum(['ios', 'tvos'])
.optional()
.default('ios')
.describe(
- 'The simulator platform to download WDA for. Default is "ios". Use "tvos" for Apple TV simulators. Note: This tool only works with simulators, not real devices.'
+ `The simulator platform to download WDA for.
+ Default is "ios".
+ Use "tvos" for Apple TV simulators.
+ Note: This tool only works with simulators, not real devices.`
),
}),
annotations: {
@@ -147,7 +157,8 @@ export default function setupWDA(server: any): void {
);
// Check if this version is already cached
- if (fs.existsSync(appPath)) {
+ try {
+ await access(appPath, constants.F_OK);
return {
content: [
{
@@ -156,14 +167,16 @@ export default function setupWDA(server: any): void {
},
],
};
+ } catch {
+ // File doesn't exist, continue to download
}
// Version not cached, download it
const startTime = Date.now();
// Create cache directories
- fs.mkdirSync(versionCacheDir, { recursive: true });
- fs.mkdirSync(extractDir, { recursive: true });
+ await mkdir(versionCacheDir, { recursive: true });
+ await mkdir(extractDir, { recursive: true });
// Download URL - use architecture-specific filename
const downloadUrl = `https://github.com/appium/WebDriverAgent/releases/download/v${wdaVersion}/WebDriverAgentRunner-Build-Sim-${archStr}.zip`;
@@ -180,7 +193,9 @@ export default function setupWDA(server: any): void {
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
// Verify extraction
- if (!fs.existsSync(appPath)) {
+ try {
+ await access(appPath, constants.F_OK);
+ } catch {
throw new Error(
'WebDriverAgent extraction failed - app bundle not found'
);
diff --git a/src/tools/upload-app.ts b/src/tools/upload-app.ts
deleted file mode 100644
index 878402e..0000000
--- a/src/tools/upload-app.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-/**
- * Tool to upload mobile apps to LambdaTest cloud storage
- */
-import { z } from 'zod';
-import fs from 'fs';
-import path from 'path';
-
-export default function uploadApp(server: any): void {
- server.addTool({
- name: 'upload_app_lambdatest',
- description:
- 'Upload a mobile app (APK/IPA) to LambdaTest cloud storage for testing',
- parameters: z.object({
- appPath: z
- .string()
- .describe(
- 'Local path to the mobile app file (APK for Android, IPA for iOS)'
- ),
- appName: z
- .string()
- .optional()
- .describe('Custom name for the app (default: filename)'),
- }),
- annotations: {
- readOnlyHint: false,
- openWorldHint: false,
- },
- execute: async (args: any, context: any): Promise => {
- try {
- const { appPath, appName } = args;
-
- // Get LambdaTest credentials
- const ltUsername = process.env.LT_USERNAME;
- const ltAccessKey = process.env.LT_ACCESS_KEY;
-
- // Validate file exists
- if (!fs.existsSync(appPath)) {
- throw new Error(`App file not found at path: ${appPath}`);
- }
-
- // Get file info
- const fileName = path.basename(appPath);
- const fileExtension = path.extname(appPath).toLowerCase();
- const finalAppName = appName || fileName;
-
- // Validate file type
- if (!['.apk', '.ipa'].includes(fileExtension)) {
- throw new Error(
- `Unsupported file type: ${fileExtension}. Only .apk and .ipa files are supported.`
- );
- }
-
- console.log(`Uploading app: ${fileName} to LambdaTest...`);
-
- // Read file as buffer
- const fileBuffer = fs.readFileSync(appPath);
-
- // Create form data boundary
- const boundary = `----formdata-jarvis-${Date.now()}`;
-
- // Create multipart form data
- const formData = [
- `--${boundary}`,
- 'Content-Disposition: form-data; name="name"',
- '',
- finalAppName,
- `--${boundary}`,
- `Content-Disposition: form-data; name="appFile"; filename="${fileName}"`,
- `Content-Type: application/${fileExtension === '.apk' ? 'vnd.android.package-archive' : 'octet-stream'}`,
- '',
- ].join('\r\n');
-
- const endBoundary = `\r\n--${boundary}--\r\n`;
-
- // Combine form data with file buffer
- const body = Buffer.concat([
- Buffer.from(formData, 'utf8'),
- fileBuffer,
- Buffer.from(endBoundary, 'utf8'),
- ]);
-
- // Upload to LambdaTest
- const response = await fetch(
- 'https://manual-api.lambdatest.com/app/upload/realDevice',
- {
- method: 'POST',
- headers: {
- Authorization: `Basic ${Buffer.from(`${ltUsername}:${ltAccessKey}`).toString('base64')}`,
- 'Content-Type': `multipart/form-data; boundary=${boundary}`,
- 'Content-Length': body.length.toString(),
- },
- body: body,
- }
- );
-
- if (!response.ok) {
- const errorText = await response.text();
- throw new Error(
- `Upload failed with status ${response.status}: ${errorText}`
- );
- }
-
- const result = await response.json();
-
- if (result.error) {
- throw new Error(`Upload failed: ${result.error}`);
- }
-
- const appUrl = result.app_url;
- const appId = appUrl.split('//')[1]; // Extract app ID from lt://APP_ID format
-
- console.log(`App uploaded successfully! App URL: ${appUrl}`);
-
- return {
- content: [
- {
- type: 'text',
- text: `App uploaded successfully to LambdaTest!
-
-Upload Details:
-- App Name: ${finalAppName}
-- File: ${fileName}
-- App URL: ${appUrl}
-- App ID: ${appId}
-
-You can now use this app URL (${appUrl}) in the 'app' parameter when creating a LambdaTest session with create_lambdatest_session tool.
-
-Example usage:
-create_lambdatest_session({
- platform: "${fileExtension === '.apk' ? 'android' : 'ios'}",
- deviceName: "Galaxy S21", // or iPhone device
- platformVersion: "11.0", // or iOS version
- app: "${appUrl}"
-})`,
- },
- ],
- };
- } catch (error: any) {
- console.error('Error uploading app to LambdaTest:', error);
-
- let errorMessage = `Failed to upload app: ${error.message}`;
-
- if (error.message.includes('ENOENT')) {
- errorMessage +=
- '\n\nTip: Check that the file path is correct and the file exists.';
- } else if (
- error.message.includes('401') ||
- error.message.includes('authentication')
- ) {
- errorMessage +=
- '\n\nTip: Check your LambdaTest credentials. You can find them at: https://accounts.lambdatest.com/security';
- } else if (
- error.message.includes('413') ||
- error.message.includes('too large')
- ) {
- errorMessage +=
- '\n\nTip: The app file might be too large. LambdaTest has file size limits for app uploads.';
- }
-
- throw new Error(errorMessage);
- }
- },
- });
-}
diff --git a/src/utils/tracing.ts b/src/utils/tracing.ts
deleted file mode 100644
index 4c4a5a9..0000000
--- a/src/utils/tracing.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import { createClient, SupabaseClient } from '@supabase/supabase-js';
-import { log } from '../locators/logger.js';
-
-export interface TraceEvent {
- timestamp: string;
- type: string;
- method?: string;
- session_id: string;
- client_id?: string;
- duration?: number;
- entity_name?: string;
- arguments?: any;
- response?: any;
- error?: string;
-}
-
-export class SupabaseTraceAdapter {
- private supabase: SupabaseClient;
- private enabled: boolean = false;
-
- constructor({ supabaseClient }: { supabaseClient: SupabaseClient }) {
- this.supabase = supabaseClient;
- this.enabled = true;
- }
-
- async logTrace(event: TraceEvent): Promise {
- if (!this.enabled) return;
-
- try {
- const { error } = await this.supabase
- .from('trace_events')
- .insert([event]);
-
- if (error) {
- log.error('Failed to log trace event:', error);
- }
- } catch (err) {
- log.error('Error logging trace event:', err);
- }
- }
-
- disable(): void {
- this.enabled = false;
- }
-
- enable(): void {
- this.enabled = true;
- }
-}
-
-export class TraceMiddleware {
- private adapter: SupabaseTraceAdapter;
- private server: any;
-
- constructor({ adapter }: { adapter: SupabaseTraceAdapter }) {
- this.adapter = adapter;
- }
-
- init(server: any): void {
- this.server = server;
- this.setupToolWrapping();
- log.info('Trace middleware initialized');
- }
-
- private setupToolWrapping(): void {
- if (!this.server) return;
-
- // Wrap the addTool method to inject tracing
- const originalAddTool = (this.server as any).addTool?.bind(this.server);
- if (originalAddTool) {
- (this.server as any).addTool = (toolDef: any) => {
- const toolName = toolDef?.name ?? 'unknown_tool';
- const originalExecute = toolDef?.execute;
-
- if (typeof originalExecute !== 'function') {
- return originalAddTool(toolDef);
- }
-
- // Wrap the execute function with tracing
- toolDef.execute = async (...args: any[]) => {
- const startTime = Date.now();
- const sessionId = this.generateSessionId();
-
- try {
- // Log tool call
- await this.adapter.logTrace({
- timestamp: new Date().toISOString(),
- type: 'tool_call',
- method: 'execute',
- session_id: sessionId,
- entity_name: toolName,
- arguments: args.length > 0 ? args[0] : {},
- });
-
- // Execute the original function
- const result = await originalExecute(...args);
-
- // Log tool response
- await this.adapter.logTrace({
- timestamp: new Date().toISOString(),
- type: 'tool_response',
- method: 'execute',
- session_id: sessionId,
- entity_name: toolName,
- duration: Date.now() - startTime,
- response: result,
- });
-
- return result;
- } catch (error: any) {
- // Log error
- await this.adapter.logTrace({
- timestamp: new Date().toISOString(),
- type: 'error',
- method: 'execute',
- session_id: sessionId,
- entity_name: toolName,
- duration: Date.now() - startTime,
- error: error?.message || 'Unknown error',
- });
-
- throw error;
- }
- };
-
- return originalAddTool(toolDef);
- };
- }
- }
-
- private generateSessionId(): string {
- return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
- }
-}