Skip to content

Commit ff37196

Browse files
committed
fix: Use z.coerce to correctly manage booleans with OpenCode and Codex
1 parent e1be10f commit ff37196

7 files changed

Lines changed: 33 additions & 16 deletions

File tree

src/tools/click.tool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { z } from 'zod';
33
import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp';
44
import type { CallToolResult } from '@modelcontextprotocol/sdk/types';
55
import type { ToolDefinition } from '../types/tool';
6+
import { coerceBoolean } from '../utils/zod-helpers';
67

78
const defaultTimeout: number = 3000;
89

@@ -11,7 +12,7 @@ export const clickToolDefinition: ToolDefinition = {
1112
description: 'clicks an element',
1213
inputSchema: {
1314
selector: z.string().describe('Value for the selector, in the form of css selector or xpath ("button.my-class" or "//button[@class=\'my-class\']" or "button=Exact text with spaces" or "a*=Link containing text")'),
14-
scrollToView: z.boolean().optional().describe('Whether to scroll the element into view before clicking').default(true),
15+
scrollToView: coerceBoolean.optional().describe('Whether to scroll the element into view before clicking').default(true),
1516
timeout: z.number().optional().describe('Maximum time to wait for element in milliseconds'),
1617
},
1718
};

src/tools/cookies.tool.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types';
33
import type { ToolDefinition } from '../types/tool';
44
import { z } from 'zod';
55
import type { Cookie } from '@wdio/protocols';
6+
import { coerceBoolean } from '../utils/zod-helpers';
67

78
export const setCookieToolDefinition: ToolDefinition = {
89
name: 'set_cookie',
@@ -13,8 +14,8 @@ export const setCookieToolDefinition: ToolDefinition = {
1314
domain: z.string().optional().describe('Cookie domain (defaults to current domain)'),
1415
path: z.string().optional().describe('Cookie path (defaults to "/")'),
1516
expiry: z.number().optional().describe('Expiry date as Unix timestamp in seconds'),
16-
httpOnly: z.boolean().optional().describe('HttpOnly flag'),
17-
secure: z.boolean().optional().describe('Secure flag'),
17+
httpOnly: coerceBoolean.optional().describe('HttpOnly flag'),
18+
secure: coerceBoolean.optional().describe('Secure flag'),
1819
sameSite: z.enum(['strict', 'lax', 'none']).optional().describe('SameSite attribute'),
1920
},
2021
};

src/tools/execute-sequence.tool.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,21 @@ import { appendStep } from '../recording/step-recorder';
1212
import { waitForStability } from '../utils/stability-detector';
1313
import { captureStateDelta } from '../utils/state-diff';
1414
import { getInteractableBrowserElements } from '../scripts/get-interactable-browser-elements';
15+
import { coerceBoolean } from '../utils/zod-helpers';
1516

1617
// Action schemas
1718
const clickActionSchema = z.object({
1819
action: z.literal('click'),
1920
selector: z.string(),
20-
scrollToView: z.boolean().optional(),
21+
scrollToView: coerceBoolean.optional(),
2122
timeout: z.number().optional(),
2223
});
2324

2425
const setValueActionSchema = z.object({
2526
action: z.literal('set_value'),
2627
selector: z.string(),
2728
value: z.string(),
28-
scrollToView: z.boolean().optional(),
29+
scrollToView: coerceBoolean.optional(),
2930
timeout: z.number().optional(),
3031
});
3132

@@ -78,7 +79,7 @@ export const executeSequenceToolDefinition: ToolDefinition = {
7879
description: 'Execute a sequence of actions atomically. Waits for page stability between actions. Returns a state delta showing what changed.',
7980
inputSchema: {
8081
actions: z.array(actionSchema).min(1).describe('Sequence of actions to execute'),
81-
waitForStability: z.boolean().optional().default(true).describe('Wait for page stability after each action'),
82+
waitForStability: coerceBoolean.optional().default(true).describe('Wait for page stability after each action'),
8283
},
8384
};
8485

src/tools/launch-chrome.tool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp';
66
import type { CallToolResult } from '@modelcontextprotocol/sdk/types';
77
import type { ToolDefinition } from '../types/tool';
88
import { z } from 'zod';
9+
import { coerceBoolean } from '../utils/zod-helpers';
910

1011
const USER_DATA_DIR = join(tmpdir(), 'chrome-debug');
1112

@@ -29,7 +30,7 @@ After this tool succeeds, call attach_browser() to connect.`,
2930
mode: z.enum(['newInstance', 'freshSession']).default('newInstance').describe(
3031
'newInstance: open alongside existing Chrome | freshSession: clean profile'
3132
),
32-
copyProfileFiles: z.boolean().default(false).describe(
33+
copyProfileFiles: coerceBoolean.default(false).describe(
3334
'Copy your Default Chrome profile (cookies, logins) into the debug session.'
3435
),
3536
},

src/tools/session.tool.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { registerSession, closeSession } from '../session/lifecycle';
88
import { localBrowserProvider } from '../providers/local-browser.provider';
99
import { localAppiumProvider } from '../providers/local-appium.provider';
1010
import type { SessionMetadata } from '../session/state';
11+
import { coerceBoolean } from '../utils/zod-helpers';
1112

1213
const platformEnum = z.enum(['browser', 'ios', 'android']);
1314
const browserEnum = z.enum(['chrome', 'firefox', 'edge', 'safari']);
@@ -19,22 +20,22 @@ export const startSessionToolDefinition: ToolDefinition = {
1920
inputSchema: {
2021
platform: platformEnum.describe('Session platform type'),
2122
browser: browserEnum.optional().describe('Browser to launch (required for browser platform)'),
22-
headless: z.boolean().optional().default(true).describe('Run browser in headless mode (default: true)'),
23+
headless: coerceBoolean.optional().default(true).describe('Run browser in headless mode (default: true)'),
2324
windowWidth: z.number().min(400).max(3840).optional().default(1920).describe('Browser window width'),
2425
windowHeight: z.number().min(400).max(2160).optional().default(1080).describe('Browser window height'),
2526
deviceName: z.string().optional().describe('Mobile device/emulator/simulator name (required for ios/android)'),
2627
platformVersion: z.string().optional().describe('OS version (e.g., "17.0", "14")'),
2728
appPath: z.string().optional().describe('Path to app file (.app/.apk/.ipa)'),
2829
automationName: automationEnum.optional().describe('Automation driver'),
29-
autoGrantPermissions: z.boolean().optional().describe('Auto-grant app permissions (default: true)'),
30-
autoAcceptAlerts: z.boolean().optional().describe('Auto-accept alerts (default: true)'),
31-
autoDismissAlerts: z.boolean().optional().describe('Auto-dismiss alerts (default: false)'),
30+
autoGrantPermissions: coerceBoolean.optional().describe('Auto-grant app permissions (default: true)'),
31+
autoAcceptAlerts: coerceBoolean.optional().describe('Auto-accept alerts (default: true)'),
32+
autoDismissAlerts: coerceBoolean.optional().describe('Auto-dismiss alerts (default: false)'),
3233
appWaitActivity: z.string().optional().describe('Activity to wait for on Android launch'),
3334
udid: z.string().optional().describe('Unique Device Identifier for iOS real device'),
34-
noReset: z.boolean().optional().describe('Preserve app data between sessions'),
35-
fullReset: z.boolean().optional().describe('Uninstall app before/after session'),
35+
noReset: coerceBoolean.optional().describe('Preserve app data between sessions'),
36+
fullReset: coerceBoolean.optional().describe('Uninstall app before/after session'),
3637
newCommandTimeout: z.number().min(0).optional().default(300).describe('Appium command timeout in seconds'),
37-
attach: z.boolean().optional().default(false).describe('Attach to existing Chrome instead of launching'),
38+
attach: coerceBoolean.optional().default(false).describe('Attach to existing Chrome instead of launching'),
3839
port: z.number().optional().default(9222).describe('Chrome remote debugging port (for attach mode)'),
3940
host: z.string().optional().default('localhost').describe('Chrome host (for attach mode)'),
4041
appiumHost: z.string().optional().describe('Appium server hostname'),
@@ -77,7 +78,7 @@ export const closeSessionToolDefinition: ToolDefinition = {
7778
name: 'close_session',
7879
description: 'Closes or detaches from the current browser or app session',
7980
inputSchema: {
80-
detach: z.boolean().optional().describe('If true, disconnect without terminating (preserves app state). Default: false'),
81+
detach: coerceBoolean.optional().describe('If true, disconnect without terminating (preserves app state). Default: false'),
8182
},
8283
};
8384

src/tools/set-value.tool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { z } from 'zod';
33
import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp';
44
import type { CallToolResult } from '@modelcontextprotocol/sdk/types';
55
import type { ToolDefinition } from '../types/tool';
6+
import { coerceBoolean } from '../utils/zod-helpers';
67

78
const defaultTimeout: number = 3000;
89

@@ -12,7 +13,7 @@ export const setValueToolDefinition: ToolDefinition = {
1213
inputSchema: {
1314
selector: z.string().describe('Value for the selector, in the form of css selector or xpath ("button.my-class" or "//button[@class=\'my-class\']")'),
1415
value: z.string().describe('Text to enter into the element'),
15-
scrollToView: z.boolean().optional().describe('Whether to scroll the element into view before typing').default(true),
16+
scrollToView: coerceBoolean.optional().describe('Whether to scroll the element into view before typing').default(true),
1617
timeout: z.number().optional().describe('Maximum time to wait for element in milliseconds'),
1718
},
1819
};

src/utils/zod-helpers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { z } from 'zod';
2+
3+
export const coerceBoolean = z.preprocess((val) => {
4+
if (typeof val === 'boolean') return val;
5+
if (typeof val === 'string') {
6+
if (val === 'false' || val === '0') return false;
7+
if (val === 'true' || val === '1') return true;
8+
return Boolean(val);
9+
}
10+
return val;
11+
}, z.boolean());

0 commit comments

Comments
 (0)