Skip to content

Commit 7bceed1

Browse files
committed
feat(simulator-management): emit structured output for keyboard toggle tools
Refactor toggle_software_keyboard and toggle_connect_hardware_keyboard to return SimulatorActionResultDomainResult, matching the xcodebuildmcp.output.simulator-action-result contract used by sibling simulator-management tools (set_sim_appearance, sim_statusbar, erase_sims, etc.). - Add toggle-software-keyboard and toggle-connect-hardware-keyboard variants to the SimulatorAction domain union and the structured output JSON schema. - Wire both tool implementations through a NonStreamingExecutor that builds a SimulatorActionResultDomainResult and publishes it via ctx.structuredOutput, replacing the prior status-line fragments. - Add outputSchema declarations to both tool manifests. - Extend the simulator-action text renderer with title and success messages for the two new action types.
1 parent 7e91100 commit 7bceed1

7 files changed

Lines changed: 215 additions & 65 deletions

File tree

manifests/tools/toggle_connect_hardware_keyboard.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ names:
44
mcp: toggle_connect_hardware_keyboard
55
cli: toggle-connect-hardware-keyboard
66
description: Toggle whether the iOS Simulator receives Mac hardware keyboard input (Cmd+Shift+K). Disconnecting makes the on-screen keyboard appear for tap-based input. Requires the simulator to be booted and Accessibility permission for the MCP host.
7+
outputSchema:
8+
schema: xcodebuildmcp.output.simulator-action-result
9+
version: "1"
710
annotations:
811
title: Toggle Connect Hardware Keyboard
912
readOnlyHint: false

manifests/tools/toggle_software_keyboard.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ names:
44
mcp: toggle_software_keyboard
55
cli: toggle-software-keyboard
66
description: Toggle the iOS Simulator software keyboard (Cmd+K). Shows or hides the on-screen keyboard. Requires the simulator to be booted and Accessibility permission for the MCP host.
7+
outputSchema:
8+
schema: xcodebuildmcp.output.simulator-action-result
9+
version: "1"
710
annotations:
811
title: Toggle Software Keyboard
912
readOnlyHint: false

schemas/structured-output/xcodebuildmcp.output.simulator-action-result/1.schema.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,30 @@
144144
"required": [
145145
"type"
146146
]
147+
},
148+
{
149+
"type": "object",
150+
"additionalProperties": false,
151+
"properties": {
152+
"type": {
153+
"const": "toggle-software-keyboard"
154+
}
155+
},
156+
"required": [
157+
"type"
158+
]
159+
},
160+
{
161+
"type": "object",
162+
"additionalProperties": false,
163+
"properties": {
164+
"type": {
165+
"const": "toggle-connect-hardware-keyboard"
166+
}
167+
},
168+
"required": [
169+
"type"
170+
]
147171
}
148172
]
149173
},

src/mcp/tools/simulator-management/toggle_connect_hardware_keyboard.ts

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,118 @@
11
import * as z from 'zod';
2+
import type { ToolHandlerContext } from '../../../rendering/types.ts';
3+
import type { SimulatorActionResultDomainResult } from '../../../types/domain-results.ts';
4+
import type { NonStreamingExecutor } from '../../../types/tool-execution.ts';
25
import { log } from '../../../utils/logging/index.ts';
36
import type { CommandExecutor } from '../../../utils/execution/index.ts';
47
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
58
import {
69
createSessionAwareTool,
710
getSessionAwareToolSchemaShape,
811
getHandlerContext,
12+
toInternalSchema,
913
} from '../../../utils/typed-tool-factory.ts';
10-
import { withErrorHandling } from '../../../utils/tool-error-handling.ts';
11-
import { header, statusLine } from '../../../utils/tool-event-builders.ts';
14+
import { toErrorMessage } from '../../../utils/errors.ts';
15+
import { createBasicDiagnostics } from '../../../utils/diagnostics.ts';
1216
import { sendKeyboardShortcut } from './_keyboard_shortcut.ts';
1317

1418
const toggleConnectHardwareKeyboardSchema = z.object({
1519
simulatorId: z.uuid().describe('UUID of the simulator to use (obtained from list_simulators)'),
1620
});
1721

1822
type ToggleConnectHardwareKeyboardParams = z.infer<typeof toggleConnectHardwareKeyboardSchema>;
23+
type ToggleConnectHardwareKeyboardResult = SimulatorActionResultDomainResult;
1924

20-
export async function toggle_connect_hardware_keyboardLogic(
21-
params: ToggleConnectHardwareKeyboardParams,
22-
executor: CommandExecutor,
23-
): Promise<void> {
24-
log('info', `Toggling hardware keyboard connection on simulator ${params.simulatorId}`);
25-
26-
const headerEvent = header('Toggle Connect Hardware Keyboard', [
27-
{ label: 'Simulator', value: params.simulatorId },
28-
]);
25+
function createToggleConnectHardwareKeyboardResult(params: {
26+
simulatorId: string;
27+
didError: boolean;
28+
error?: string;
29+
diagnosticMessage?: string;
30+
}): ToggleConnectHardwareKeyboardResult {
31+
return {
32+
kind: 'simulator-action-result',
33+
didError: params.didError,
34+
error: params.error ?? null,
35+
summary: {
36+
status: params.didError ? 'FAILED' : 'SUCCEEDED',
37+
},
38+
action: {
39+
type: 'toggle-connect-hardware-keyboard',
40+
},
41+
...(params.diagnosticMessage
42+
? { diagnostics: createBasicDiagnostics({ errors: [params.diagnosticMessage] }) }
43+
: {}),
44+
artifacts: {
45+
simulatorId: params.simulatorId,
46+
},
47+
};
48+
}
2949

30-
const ctx = getHandlerContext();
50+
function setStructuredOutput(
51+
ctx: ToolHandlerContext,
52+
result: ToggleConnectHardwareKeyboardResult,
53+
): void {
54+
ctx.structuredOutput = {
55+
result,
56+
schema: 'xcodebuildmcp.output.simulator-action-result',
57+
schemaVersion: '1',
58+
};
59+
}
3160

32-
return withErrorHandling(
33-
ctx,
34-
async () => {
61+
export function createToggleConnectHardwareKeyboardExecutor(
62+
executor: CommandExecutor,
63+
): NonStreamingExecutor<ToggleConnectHardwareKeyboardParams, ToggleConnectHardwareKeyboardResult> {
64+
return async (params) => {
65+
try {
3566
const result = await sendKeyboardShortcut(
3667
params.simulatorId,
3768
'connect-hardware-keyboard',
3869
executor,
3970
);
4071

4172
if (!result.success) {
42-
log('error', `Failed to toggle hardware keyboard: ${result.error}`);
43-
ctx.emit(headerEvent);
44-
ctx.emit(statusLine('error', result.error));
45-
return;
73+
return createToggleConnectHardwareKeyboardResult({
74+
simulatorId: params.simulatorId,
75+
didError: true,
76+
error: 'Failed to toggle hardware keyboard.',
77+
diagnosticMessage: result.error,
78+
});
4679
}
4780

48-
ctx.emit(headerEvent);
49-
ctx.emit(statusLine('success', 'Sent Connect Hardware Keyboard (Cmd+Shift+K)'));
50-
},
51-
{
52-
header: headerEvent,
53-
errorMessage: ({ message }) => `Failed to toggle hardware keyboard: ${message}`,
54-
logMessage: ({ message }) =>
55-
`Error toggling hardware keyboard for simulator ${params.simulatorId}: ${message}`,
56-
},
57-
);
81+
return createToggleConnectHardwareKeyboardResult({
82+
simulatorId: params.simulatorId,
83+
didError: false,
84+
});
85+
} catch (error) {
86+
const diagnosticMessage = toErrorMessage(error);
87+
return createToggleConnectHardwareKeyboardResult({
88+
simulatorId: params.simulatorId,
89+
didError: true,
90+
error: 'Failed to toggle hardware keyboard.',
91+
diagnosticMessage,
92+
});
93+
}
94+
};
95+
}
96+
97+
export async function toggle_connect_hardware_keyboardLogic(
98+
params: ToggleConnectHardwareKeyboardParams,
99+
executor: CommandExecutor,
100+
): Promise<void> {
101+
log('info', `Toggling hardware keyboard connection on simulator ${params.simulatorId}`);
102+
103+
const ctx = getHandlerContext();
104+
const executeToggleConnectHardwareKeyboard =
105+
createToggleConnectHardwareKeyboardExecutor(executor);
106+
107+
const result = await executeToggleConnectHardwareKeyboard(params);
108+
setStructuredOutput(ctx, result);
109+
110+
if (result.didError) {
111+
log(
112+
'error',
113+
`Error toggling hardware keyboard for simulator ${params.simulatorId}: ${result.error ?? 'Unknown error'}`,
114+
);
115+
}
58116
}
59117

60118
const publicSchemaObject = z.strictObject(
@@ -67,10 +125,9 @@ export const schema = getSessionAwareToolSchemaShape({
67125
});
68126

69127
export const handler = createSessionAwareTool<ToggleConnectHardwareKeyboardParams>({
70-
internalSchema: toggleConnectHardwareKeyboardSchema as unknown as z.ZodType<
71-
ToggleConnectHardwareKeyboardParams,
72-
unknown
73-
>,
128+
internalSchema: toInternalSchema<ToggleConnectHardwareKeyboardParams>(
129+
toggleConnectHardwareKeyboardSchema,
130+
),
74131
logicFunction: toggle_connect_hardware_keyboardLogic,
75132
getExecutor: getDefaultCommandExecutor,
76133
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],

src/mcp/tools/simulator-management/toggle_software_keyboard.ts

Lines changed: 82 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,110 @@
11
import * as z from 'zod';
2+
import type { ToolHandlerContext } from '../../../rendering/types.ts';
3+
import type { SimulatorActionResultDomainResult } from '../../../types/domain-results.ts';
4+
import type { NonStreamingExecutor } from '../../../types/tool-execution.ts';
25
import { log } from '../../../utils/logging/index.ts';
36
import type { CommandExecutor } from '../../../utils/execution/index.ts';
47
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
58
import {
69
createSessionAwareTool,
710
getSessionAwareToolSchemaShape,
811
getHandlerContext,
12+
toInternalSchema,
913
} from '../../../utils/typed-tool-factory.ts';
10-
import { withErrorHandling } from '../../../utils/tool-error-handling.ts';
11-
import { header, statusLine } from '../../../utils/tool-event-builders.ts';
14+
import { toErrorMessage } from '../../../utils/errors.ts';
15+
import { createBasicDiagnostics } from '../../../utils/diagnostics.ts';
1216
import { sendKeyboardShortcut } from './_keyboard_shortcut.ts';
1317

1418
const toggleSoftwareKeyboardSchema = z.object({
1519
simulatorId: z.uuid().describe('UUID of the simulator to use (obtained from list_simulators)'),
1620
});
1721

1822
type ToggleSoftwareKeyboardParams = z.infer<typeof toggleSoftwareKeyboardSchema>;
23+
type ToggleSoftwareKeyboardResult = SimulatorActionResultDomainResult;
24+
25+
function createToggleSoftwareKeyboardResult(params: {
26+
simulatorId: string;
27+
didError: boolean;
28+
error?: string;
29+
diagnosticMessage?: string;
30+
}): ToggleSoftwareKeyboardResult {
31+
return {
32+
kind: 'simulator-action-result',
33+
didError: params.didError,
34+
error: params.error ?? null,
35+
summary: {
36+
status: params.didError ? 'FAILED' : 'SUCCEEDED',
37+
},
38+
action: {
39+
type: 'toggle-software-keyboard',
40+
},
41+
...(params.diagnosticMessage
42+
? { diagnostics: createBasicDiagnostics({ errors: [params.diagnosticMessage] }) }
43+
: {}),
44+
artifacts: {
45+
simulatorId: params.simulatorId,
46+
},
47+
};
48+
}
49+
50+
function setStructuredOutput(ctx: ToolHandlerContext, result: ToggleSoftwareKeyboardResult): void {
51+
ctx.structuredOutput = {
52+
result,
53+
schema: 'xcodebuildmcp.output.simulator-action-result',
54+
schemaVersion: '1',
55+
};
56+
}
57+
58+
export function createToggleSoftwareKeyboardExecutor(
59+
executor: CommandExecutor,
60+
): NonStreamingExecutor<ToggleSoftwareKeyboardParams, ToggleSoftwareKeyboardResult> {
61+
return async (params) => {
62+
try {
63+
const result = await sendKeyboardShortcut(params.simulatorId, 'software-keyboard', executor);
64+
65+
if (!result.success) {
66+
return createToggleSoftwareKeyboardResult({
67+
simulatorId: params.simulatorId,
68+
didError: true,
69+
error: 'Failed to toggle software keyboard.',
70+
diagnosticMessage: result.error,
71+
});
72+
}
73+
74+
return createToggleSoftwareKeyboardResult({
75+
simulatorId: params.simulatorId,
76+
didError: false,
77+
});
78+
} catch (error) {
79+
const diagnosticMessage = toErrorMessage(error);
80+
return createToggleSoftwareKeyboardResult({
81+
simulatorId: params.simulatorId,
82+
didError: true,
83+
error: 'Failed to toggle software keyboard.',
84+
diagnosticMessage,
85+
});
86+
}
87+
};
88+
}
1989

2090
export async function toggle_software_keyboardLogic(
2191
params: ToggleSoftwareKeyboardParams,
2292
executor: CommandExecutor,
2393
): Promise<void> {
2494
log('info', `Toggling software keyboard on simulator ${params.simulatorId}`);
2595

26-
const headerEvent = header('Toggle Software Keyboard', [
27-
{ label: 'Simulator', value: params.simulatorId },
28-
]);
29-
3096
const ctx = getHandlerContext();
97+
const executeToggleSoftwareKeyboard = createToggleSoftwareKeyboardExecutor(executor);
3198

32-
return withErrorHandling(
33-
ctx,
34-
async () => {
35-
const result = await sendKeyboardShortcut(params.simulatorId, 'software-keyboard', executor);
99+
const result = await executeToggleSoftwareKeyboard(params);
100+
setStructuredOutput(ctx, result);
36101

37-
if (!result.success) {
38-
log('error', `Failed to toggle software keyboard: ${result.error}`);
39-
ctx.emit(headerEvent);
40-
ctx.emit(statusLine('error', result.error));
41-
return;
42-
}
43-
44-
ctx.emit(headerEvent);
45-
ctx.emit(statusLine('success', 'Sent Toggle Software Keyboard (Cmd+K)'));
46-
},
47-
{
48-
header: headerEvent,
49-
errorMessage: ({ message }) => `Failed to toggle software keyboard: ${message}`,
50-
logMessage: ({ message }) =>
51-
`Error toggling software keyboard for simulator ${params.simulatorId}: ${message}`,
52-
},
53-
);
102+
if (result.didError) {
103+
log(
104+
'error',
105+
`Error toggling software keyboard for simulator ${params.simulatorId}: ${result.error ?? 'Unknown error'}`,
106+
);
107+
}
54108
}
55109

56110
const publicSchemaObject = z.strictObject(
@@ -63,10 +117,7 @@ export const schema = getSessionAwareToolSchemaShape({
63117
});
64118

65119
export const handler = createSessionAwareTool<ToggleSoftwareKeyboardParams>({
66-
internalSchema: toggleSoftwareKeyboardSchema as unknown as z.ZodType<
67-
ToggleSoftwareKeyboardParams,
68-
unknown
69-
>,
120+
internalSchema: toInternalSchema<ToggleSoftwareKeyboardParams>(toggleSoftwareKeyboardSchema),
70121
logicFunction: toggle_software_keyboardLogic,
71122
getExecutor: getDefaultCommandExecutor,
72123
requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }],

src/types/domain-results.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,14 +409,22 @@ export interface SimulatorActionStatusbar {
409409
type: 'statusbar';
410410
dataNetwork?: string;
411411
}
412+
export interface SimulatorActionToggleSoftwareKeyboard {
413+
type: 'toggle-software-keyboard';
414+
}
415+
export interface SimulatorActionToggleConnectHardwareKeyboard {
416+
type: 'toggle-connect-hardware-keyboard';
417+
}
412418
export type SimulatorAction =
413419
| SimulatorActionBoot
414420
| SimulatorActionErase
415421
| SimulatorActionOpen
416422
| SimulatorActionResetLocation
417423
| SimulatorActionSetLocation
418424
| SimulatorActionSetAppearance
419-
| SimulatorActionStatusbar;
425+
| SimulatorActionStatusbar
426+
| SimulatorActionToggleSoftwareKeyboard
427+
| SimulatorActionToggleConnectHardwareKeyboard;
420428
export interface AppPathRequest {
421429
scheme?: string;
422430
projectPath?: string;

0 commit comments

Comments
 (0)