Skip to content

Commit 80e8bfa

Browse files
committed
feat: Reintroduce tooling for a11y, app_state, contexts, cookies, tabs and screenshot querying
1 parent 4906b38 commit 80e8bfa

14 files changed

Lines changed: 160 additions & 11 deletions

src/resources/app-state.resource.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@ import type { ResourceDefinition } from '../types/resource';
22
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
33
import { getBrowser } from '../session/state';
44

5-
async function readAppState(bundleId: string): Promise<{ mimeType: string; text: string }> {
5+
export async function readAppState(bundleId: string): Promise<{ mimeType: string; text: string }> {
66
try {
77
const browser = getBrowser();
88

9+
if (!browser.isMobile) {
10+
return {
11+
mimeType: 'text/plain',
12+
text: 'Error: get_app_state is mobile-only. Use it on an iOS or Android session.'
13+
};
14+
}
15+
916
const appIdentifier = browser.isAndroid
1017
? { appId: bundleId }
1118
: { bundleId: bundleId };
@@ -34,7 +41,7 @@ export const appStateResource: ResourceDefinition = {
3441
template: new ResourceTemplate('wdio://session/current/app-state/{bundleId}', { list: undefined }),
3542
description: 'App state for a given bundle ID',
3643
handler: async (uri, variables) => {
37-
const result = await readAppState(variables.bundleId as string);
44+
const result = await readAppState(variables.bundleId);
3845
return { contents: [{ uri: uri.href, mimeType: result.mimeType, text: result.text }] };
3946
},
4047
};

src/resources/contexts.resource.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ResourceDefinition } from '../types/resource';
22
import { getBrowser } from '../session/state';
33

4-
async function readContexts(): Promise<{ mimeType: string; text: string }> {
4+
export async function readContexts(): Promise<{ mimeType: string; text: string }> {
55
try {
66
const browser = getBrowser();
77
const contexts = await browser.getContexts();
@@ -11,7 +11,7 @@ async function readContexts(): Promise<{ mimeType: string; text: string }> {
1111
}
1212
}
1313

14-
async function readCurrentContext(): Promise<{ mimeType: string; text: string }> {
14+
export async function readCurrentContext(): Promise<{ mimeType: string; text: string }> {
1515
try {
1616
const browser = getBrowser();
1717
const currentContext = await browser.getContext();

src/resources/cookies.resource.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ResourceDefinition } from '../types/resource';
22
import { getBrowser } from '../session/state';
33

4-
async function readCookies(name?: string): Promise<{ mimeType: string; text: string }> {
4+
export async function readCookies(name?: string): Promise<{ mimeType: string; text: string }> {
55
try {
66
const browser = getBrowser();
77

@@ -15,7 +15,7 @@ async function readCookies(name?: string): Promise<{ mimeType: string; text: str
1515
const cookies = await browser.getCookies();
1616
return { mimeType: 'application/json', text: JSON.stringify(cookies) };
1717
} catch (e) {
18-
return { mimeType: 'application/json', text: JSON.stringify({ error: String(e) }) };
18+
return { mimeType: 'text/plain', text: `Error: ${e}` };
1919
}
2020
}
2121

src/resources/screenshot.resource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async function processScreenshot(screenshotBase64: string): Promise<{ data: Buff
3232
return { data: outputBuffer, mimeType: 'image/png' };
3333
}
3434

35-
async function readScreenshot(): Promise<{ mimeType: string; blob: string }> {
35+
export async function readScreenshot(): Promise<{ mimeType: string; blob: string }> {
3636
try {
3737
const browser = getBrowser();
3838
const screenshot = await browser.takeScreenshot();

src/resources/tabs.resource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ResourceDefinition } from '../types/resource';
22
import { getBrowser } from '../session/state';
33

4-
async function readTabs(): Promise<{ mimeType: string; text: string }> {
4+
export async function readTabs(): Promise<{ mimeType: string; text: string }> {
55
try {
66
const browser = getBrowser();
77
const handles = await browser.getWindowHandles();

src/server.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ import {
6868
uploadAppTool,
6969
uploadAppToolDefinition,
7070
} from './tools/browserstack.tool';
71+
import { screenshotTool, screenshotToolDefinition } from './tools/screenshot.tool';
72+
import { accessibilityTool, accessibilityToolDefinition } from './tools/accessibility.tool';
73+
import { getTabsTool, getTabsToolDefinition } from './tools/get-tabs.tool';
74+
import { getContextsTool, getContextsToolDefinition } from './tools/get-contexts.tool';
75+
import { appStateTool, appStateToolDefinition } from './tools/app-state.tool';
76+
import { getCookiesTool, getCookiesToolDefinition } from './tools/get-cookies.tool';
7177

7278
console.log = (...args) => console.error('[LOG]', ...args);
7379
console.info = (...args) => console.error('[INFO]', ...args);
@@ -144,6 +150,13 @@ registerTool(getElementsToolDefinition, getElementsTool);
144150
registerTool(listAppsToolDefinition, listAppsTool);
145151
registerTool(uploadAppToolDefinition, uploadAppTool);
146152

153+
registerTool(screenshotToolDefinition, screenshotTool);
154+
registerTool(accessibilityToolDefinition, accessibilityTool);
155+
registerTool(getTabsToolDefinition, getTabsTool);
156+
registerTool(getContextsToolDefinition, getContextsTool);
157+
registerTool(appStateToolDefinition, appStateTool);
158+
registerTool(getCookiesToolDefinition, getCookiesTool);
159+
147160
registerResource(sessionsIndexResource);
148161
registerResource(sessionCurrentStepsResource);
149162
registerResource(sessionCurrentCodeResource);

src/tools/accessibility.tool.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3+
import type { ToolDefinition } from '../types/tool';
4+
import { z } from 'zod';
5+
import { readAccessibilityTree } from '../resources';
6+
7+
export const accessibilityToolDefinition: ToolDefinition = {
8+
name: 'get_accessibility_tree',
9+
description: 'Returns the page accessibility tree with roles, names, and selectors. Browser-only. Supports filtering by ARIA roles and pagination via limit/offset.',
10+
inputSchema: {
11+
limit: z.number().optional().default(0).describe('Maximum number of nodes to return (0 = no limit)'),
12+
offset: z.number().optional().default(0).describe('Number of nodes to skip for pagination'),
13+
roles: z.array(z.string()).optional().describe('Filter by ARIA roles, e.g. ["button", "link", "heading"]'),
14+
},
15+
};
16+
17+
export const accessibilityTool: ToolCallback = async ({ limit = 0, offset = 0, roles }: {
18+
limit?: number;
19+
offset?: number;
20+
roles?: string[];
21+
}): Promise<CallToolResult> => {
22+
const result = await readAccessibilityTree({ limit, offset, roles });
23+
if (result.text.startsWith('Error')) {
24+
return { isError: true, content: [{ type: 'text', text: result.text }] };
25+
}
26+
return { content: [{ type: 'text', text: result.text }] };
27+
};

src/tools/app-state.tool.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3+
import type { ToolDefinition } from '../types/tool';
4+
import { z } from 'zod';
5+
import { readAppState } from '../resources';
6+
7+
export const appStateToolDefinition: ToolDefinition = {
8+
name: 'get_app_state',
9+
description: 'Returns the current state of a mobile app: not installed, not running, background, or foreground. Mobile-only.',
10+
inputSchema: {
11+
bundleId: z.string().describe('App bundle ID (iOS) or package name (Android), e.g. "com.example.app"'),
12+
},
13+
};
14+
15+
export const appStateTool: ToolCallback = async ({ bundleId }: { bundleId: string }): Promise<CallToolResult> => {
16+
const result = await readAppState(bundleId);
17+
if (result.text.startsWith('Error')) {
18+
return { isError: true, content: [{ type: 'text', text: result.text }] };
19+
}
20+
return { content: [{ type: 'text', text: result.text }] };
21+
};

src/tools/context.tool.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { getBrowser } from '../session/state';
66

77
export const switchContextToolDefinition: ToolDefinition = {
88
name: 'switch_context',
9-
description: 'Switches between native and webview automation contexts in a hybrid mobile app. Required before using CSS/XPath selectors inside an embedded webview — switch to WEBVIEW_* first, then switch back to NATIVE_APP for native elements. List available contexts from wdio://session/current/contexts.',
9+
description: 'Switches between native and webview automation contexts in a hybrid mobile app. Required before using CSS/XPath selectors inside an embedded webview — switch to WEBVIEW_* first, then switch back to NATIVE_APP for native elements. List available contexts using get_contexts tool or wdio://session/current/contexts resource.',
1010
inputSchema: {
1111
context: z
1212
.string()
@@ -32,7 +32,6 @@ export const switchContextTool: ToolCallback = async (args: {
3232
return { content: [{ type: 'text', text: `Switched to context: ${targetContext}` }] };
3333
}
3434
throw new Error(`Error: Invalid context index ${context}. Available contexts: ${contexts.length}`);
35-
3635
}
3736

3837
await browser.switchContext(context);

src/tools/get-contexts.tool.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3+
import type { ToolDefinition } from '../types/tool';
4+
import { readContexts, readCurrentContext } from '../resources';
5+
6+
export const getContextsToolDefinition: ToolDefinition = {
7+
name: 'get_contexts',
8+
description: 'Returns available automation contexts and the currently active one. Use before switch_context to discover NATIVE_APP and WEBVIEW_* targets. Mobile-only.',
9+
inputSchema: {},
10+
};
11+
12+
export const getContextsTool: ToolCallback = async (): Promise<CallToolResult> => {
13+
const [contexts, current] = await Promise.all([readContexts(), readCurrentContext()]);
14+
if (contexts.mimeType === 'text/plain' && contexts.text.startsWith('Error')) {
15+
return { isError: true, content: [{ type: 'text', text: contexts.text }] };
16+
}
17+
if (current.mimeType === 'text/plain' && current.text.startsWith('Error')) {
18+
return { isError: true, content: [{ type: 'text', text: current.text }] };
19+
}
20+
const combined = {
21+
contexts: JSON.parse(contexts.text),
22+
currentContext: JSON.parse(current.text),
23+
};
24+
return { content: [{ type: 'text', text: JSON.stringify(combined) }] };
25+
};

0 commit comments

Comments
 (0)