Skip to main content
Glama

Mobile Next MCP

webdriver-agent.ts10.8 kB
import { ActionableError, SwipeDirection, ScreenSize, ScreenElement, Orientation } from "./robot"; export interface SourceTreeElementRect { x: number; y: number; width: number; height: number; } export interface SourceTreeElement { type: string; label?: string; name?: string; value?: string; rawIdentifier?: string; rect: SourceTreeElementRect; isVisible?: string; // "0" or "1" children?: Array<SourceTreeElement>; } export interface SourceTree { value: SourceTreeElement; } export class WebDriverAgent { constructor(private readonly host: string, private readonly port: number) { } public async isRunning(): Promise<boolean> { const url = `http://${this.host}:${this.port}/status`; try { const response = await fetch(url); return response.status === 200; } catch (error) { console.error(`Failed to connect to WebDriverAgent: ${error}`); return false; } } public async createSession(): Promise<string> { const url = `http://${this.host}:${this.port}/session`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ capabilities: { alwaysMatch: { platformName: "iOS" } } }), }); if (!response.ok) { const errorText = await response.text(); throw new ActionableError(`Failed to create WebDriver session: ${response.status} ${errorText}`); } const json = await response.json(); if (!json.value || !json.value.sessionId) { throw new ActionableError(`Invalid session response: ${JSON.stringify(json)}`); } return json.value.sessionId; } public async deleteSession(sessionId: string) { const url = `http://${this.host}:${this.port}/session/${sessionId}`; const response = await fetch(url, { method: "DELETE" }); return response.json(); } public async withinSession(fn: (url: string) => Promise<any>) { const sessionId = await this.createSession(); const url = `http://${this.host}:${this.port}/session/${sessionId}`; const result = await fn(url); await this.deleteSession(sessionId); return result; } public async getScreenSize(sessionUrl?: string): Promise<ScreenSize> { if (sessionUrl) { const url = `${sessionUrl}/wda/screen`; const response = await fetch(url); const json = await response.json(); return { width: json.value.screenSize.width, height: json.value.screenSize.height, scale: json.value.scale || 1, }; } else { return this.withinSession(async sessionUrlInner => { const url = `${sessionUrlInner}/wda/screen`; const response = await fetch(url); const json = await response.json(); return { width: json.value.screenSize.width, height: json.value.screenSize.height, scale: json.value.scale || 1, }; }); } } public async sendKeys(keys: string) { await this.withinSession(async sessionUrl => { const url = `${sessionUrl}/wda/keys`; await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ value: [keys] }), }); }); } public async pressButton(button: string) { const _map = { "HOME": "home", "VOLUME_UP": "volumeup", "VOLUME_DOWN": "volumedown", }; if (button === "ENTER") { await this.sendKeys("\n"); return; } // Type assertion to check if button is a key of _map if (!(button in _map)) { throw new ActionableError(`Button "${button}" is not supported`); } await this.withinSession(async sessionUrl => { const url = `${sessionUrl}/wda/pressButton`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name: button, }), }); return response.json(); }); } public async tap(x: number, y: number) { await this.withinSession(async sessionUrl => { const url = `${sessionUrl}/actions`; await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ actions: [ { type: "pointer", id: "finger1", parameters: { pointerType: "touch" }, actions: [ { type: "pointerMove", duration: 0, x, y }, { type: "pointerDown", button: 0 }, { type: "pause", duration: 100 }, { type: "pointerUp", button: 0 } ] } ] }), }); }); } private isVisible(rect: SourceTreeElementRect): boolean { return rect.x >= 0 && rect.y >= 0; } private filterSourceElements(source: SourceTreeElement): Array<ScreenElement> { const output: ScreenElement[] = []; const acceptedTypes = ["TextField", "Button", "Switch", "Icon", "SearchField", "StaticText", "Image"]; if (acceptedTypes.includes(source.type)) { if (source.isVisible === "1" && this.isVisible(source.rect)) { if (source.label !== null || source.name !== null || source.rawIdentifier !== null) { output.push({ type: source.type, label: source.label, name: source.name, value: source.value, identifier: source.rawIdentifier, rect: { x: source.rect.x, y: source.rect.y, width: source.rect.width, height: source.rect.height, }, }); } } } if (source.children) { for (const child of source.children) { output.push(...this.filterSourceElements(child)); } } return output; } public async getPageSource(): Promise<SourceTree> { const url = `http://${this.host}:${this.port}/source/?format=json`; const response = await fetch(url); const json = await response.json(); return json as SourceTree; } public async getElementsOnScreen(): Promise<ScreenElement[]> { const source = await this.getPageSource(); return this.filterSourceElements(source.value); } public async openUrl(url: string): Promise<void> { await this.withinSession(async sessionUrl => { await fetch(`${sessionUrl}/url`, { method: "POST", body: JSON.stringify({ url }), }); }); } public async getScreenshot(): Promise<Buffer> { const url = `http://${this.host}:${this.port}/screenshot`; const response = await fetch(url); const json = await response.json(); return Buffer.from(json.value, "base64"); } public async swipe(direction: SwipeDirection): Promise<void> { await this.withinSession(async sessionUrl => { const screenSize = await this.getScreenSize(sessionUrl); let x0: number, y0: number, x1: number, y1: number; // Use 60% of the width/height for swipe distance const verticalDistance = Math.floor(screenSize.height * 0.6); const horizontalDistance = Math.floor(screenSize.width * 0.6); const centerX = Math.floor(screenSize.width / 2); const centerY = Math.floor(screenSize.height / 2); switch (direction) { case "up": x0 = x1 = centerX; y0 = centerY + Math.floor(verticalDistance / 2); y1 = centerY - Math.floor(verticalDistance / 2); break; case "down": x0 = x1 = centerX; y0 = centerY - Math.floor(verticalDistance / 2); y1 = centerY + Math.floor(verticalDistance / 2); break; case "left": y0 = y1 = centerY; x0 = centerX + Math.floor(horizontalDistance / 2); x1 = centerX - Math.floor(horizontalDistance / 2); break; case "right": y0 = y1 = centerY; x0 = centerX - Math.floor(horizontalDistance / 2); x1 = centerX + Math.floor(horizontalDistance / 2); break; default: throw new ActionableError(`Swipe direction "${direction}" is not supported`); } const url = `${sessionUrl}/actions`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ actions: [ { type: "pointer", id: "finger1", parameters: { pointerType: "touch" }, actions: [ { type: "pointerMove", duration: 0, x: x0, y: y0 }, { type: "pointerDown", button: 0 }, { type: "pointerMove", duration: 1000, x: x1, y: y1 }, { type: "pointerUp", button: 0 } ] } ] }), }); if (!response.ok) { const errorText = await response.text(); throw new ActionableError(`WebDriver actions request failed: ${response.status} ${errorText}`); } // Clear actions to ensure they complete await fetch(`${sessionUrl}/actions`, { method: "DELETE", }); }); } public async swipeFromCoordinate(x: number, y: number, direction: SwipeDirection, distance: number = 400): Promise<void> { await this.withinSession(async sessionUrl => { // Use simple coordinates like the working swipe method const x0 = x; const y0 = y; let x1 = x; let y1 = y; // Calculate target position based on direction and distance switch (direction) { case "up": y1 = y - distance; // Move up by specified distance break; case "down": y1 = y + distance; // Move down by specified distance break; case "left": x1 = x - distance; // Move left by specified distance break; case "right": x1 = x + distance; // Move right by specified distance break; default: throw new ActionableError(`Swipe direction "${direction}" is not supported`); } const url = `${sessionUrl}/actions`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ actions: [ { type: "pointer", id: "finger1", parameters: { pointerType: "touch" }, actions: [ { type: "pointerMove", duration: 0, x: x0, y: y0 }, { type: "pointerDown", button: 0 }, { type: "pointerMove", duration: 1000, x: x1, y: y1 }, { type: "pointerUp", button: 0 } ] } ] }), }); if (!response.ok) { const errorText = await response.text(); throw new ActionableError(`WebDriver actions request failed: ${response.status} ${errorText}`); } // Clear actions to ensure they complete await fetch(`${sessionUrl}/actions`, { method: "DELETE", }); }); } public async setOrientation(orientation: Orientation): Promise<void> { await this.withinSession(async sessionUrl => { const url = `${sessionUrl}/orientation`; await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ orientation: orientation.toUpperCase() }) }); }); } public async getOrientation(): Promise<Orientation> { return this.withinSession(async sessionUrl => { const url = `${sessionUrl}/orientation`; const response = await fetch(url); const json = await response.json(); return json.value.toLowerCase() as Orientation; }); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/EmpathySlainLovers/MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server