Skip to content

Commit e90292e

Browse files
committed
feat: inherit MCP descriptions from CLI help
1 parent 2707a31 commit e90292e

10 files changed

Lines changed: 69 additions & 1 deletion

File tree

src/commands/__tests__/command-surface-metadata.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ test('CI-only prepare command stays out of MCP tool surface', () => {
4141
assert.equal(listMcpExposedCommandNames().includes('prepare'), false);
4242
});
4343

44+
test('MCP tool descriptions inherit complete CLI guidance', () => {
45+
const cliSchemas = listCommandFamilyCliSchemas();
46+
const definitionsByName = new Map(
47+
listCommandFamilyDefinitions().map((definition) => [definition.name, definition] as const),
48+
);
49+
50+
for (const metadata of listMcpCommandMetadata()) {
51+
const cliSchema = cliSchemas[metadata.name];
52+
const cliDescription = cliSchema?.helpDescription ?? cliSchema?.summary;
53+
assert.ok(cliDescription, `${metadata.name} must define CLI guidance for its MCP description`);
54+
assert.equal(metadata.description, cliDescription);
55+
assert.equal(definitionsByName.get(metadata.name)?.description, cliDescription);
56+
}
57+
});
58+
4459
test('common command input accepts web platform selector', () => {
4560
const snapshotMetadata = listCommandMetadata().find((metadata) => metadata.name === 'snapshot');
4661
if (!snapshotMetadata) throw new Error('Expected snapshot command metadata');

src/commands/capture/alert.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const alertCommandDefinition = defineExecutableCommand(alertCommandMetadata, (cl
3434

3535
const alertCliSchema = {
3636
usageOverride: 'alert [get|accept|dismiss|wait] [timeout]',
37+
helpDescription:
38+
'Inspect, wait for, accept, or dismiss a platform alert. Use get before acting when the alert content matters; accept and dismiss change the active alert state.',
3739
positionalArgs: ['action?', 'timeout?'],
3840
} as const;
3941

src/commands/capture/wait.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const waitCommandDefinition = defineExecutableCommand(waitCommandMetadata, (clie
4949

5050
const waitCliSchema = {
5151
usageOverride: 'wait <ms>|text <text>|@ref|<selector>|stable [quietMs] [timeoutMs]',
52+
helpDescription:
53+
'Wait for a duration, text, snapshot ref, selector, or stable UI. Use text, ref, or selector for a specific readiness condition; stable waits until the UI stays quiet for the requested window.',
5254
positionalArgs: ['durationOrSelector', 'timeoutMs?'],
5355
allowsExtraPositionals: true,
5456
allowedFlags: [...SELECTOR_SNAPSHOT_FLAGS],

src/commands/family/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,16 @@ export function defineCommandFacet<
6262
const TCommandName extends string,
6363
const TCommand extends CommandFacet<TCommandName>,
6464
>(command: TCommand): TCommand {
65-
return command;
65+
const description = command.cliSchema?.helpDescription ?? command.cliSchema?.summary;
66+
if (!description) return command;
67+
68+
// CLI help is the command-surface owner for agent-facing guidance. Keep MCP
69+
// tool descriptions aligned by default so the two projections cannot drift.
70+
return {
71+
...command,
72+
metadata: { ...command.metadata, description },
73+
definition: { ...command.definition, description },
74+
} as TCommand;
6675
}
6776

6877
export function defineCommandFamilyFromFacets<

src/commands/interaction/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ import { selectorCliReaders, selectorDaemonWriters } from './selectors.ts';
6161
const interactionCliSchemas = {
6262
get: {
6363
usageOverride: 'get text|attrs <@ref|selector>',
64+
helpDescription:
65+
'Read text or accessibility attributes from a snapshot ref or selector without changing the app. Use format text for visible content or attrs for the element attribute map.',
6466
positionalArgs: ['subcommand', 'target'],
6567
allowsExtraPositionals: true,
6668
allowedFlags: [...SELECTOR_SNAPSHOT_FLAGS, 'record'],
@@ -74,12 +76,16 @@ const interactionCliSchemas = {
7476
allowedFlags: ['snapshotDepth', 'snapshotRaw', 'findFirst', 'findLast', 'record'],
7577
},
7678
is: {
79+
helpDescription:
80+
'Check whether a selector satisfies a UI predicate such as visible, hidden, editable, selected, focused, or text. Use wait when the condition may appear asynchronously.',
7781
positionalArgs: ['predicate', 'selector', 'value?'],
7882
allowsExtraPositionals: true,
7983
allowedFlags: [...SELECTOR_SNAPSHOT_FLAGS, 'record'],
8084
},
8185
click: {
8286
usageOverride: 'click <x y|@ref|selector>',
87+
helpDescription:
88+
'Activate a UI target by snapshot ref, selector, or coordinates. Prefer a ref or selector after snapshot; use coordinates only when semantic targeting is unavailable. This can change app state; use settle or snapshot to verify the result.',
8389
positionalArgs: ['target'],
8490
allowsExtraPositionals: true,
8591
allowedFlags: [
@@ -128,15 +134,21 @@ const interactionCliSchemas = {
128134
allowedFlags: ['pointerCount'],
129135
},
130136
focus: {
137+
helpDescription:
138+
'Move input focus to explicit screen coordinates without entering text. Prefer semantic interactions when a snapshot ref or selector is available; use type or fill after focus.',
131139
positionalArgs: ['x', 'y'],
132140
},
133141
type: {
142+
helpDescription:
143+
'Append text to the currently focused input. Use fill when the existing field value should be replaced, and focus first when no input is active.',
134144
positionalArgs: ['text'],
135145
allowsExtraPositionals: true,
136146
allowedFlags: ['delayMs'],
137147
},
138148
fill: {
139149
usageOverride: 'fill <x> <y> <text> | fill <@ref|selector> <text>',
150+
helpDescription:
151+
'Replace text in a UI input selected by snapshot ref, selector, or coordinates. Prefer refs or selectors after snapshot; use recordAs to keep sensitive text out of a recorded replay while sending it to the live app.',
140152
positionalArgs: ['targetOrX', 'yOrText', 'text?'],
141153
allowsExtraPositionals: true,
142154
allowedFlags: [

src/commands/management/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ const openCliSchema = {
128128
} as const satisfies CommandSchemaOverride;
129129

130130
const closeCliSchema = {
131+
helpDescription:
132+
'Close the named app, or close the active session app when app is omitted. Use shutdown only when the selected simulator or emulator should also stop.',
131133
positionalArgs: ['app?'],
132134
allowedFlags: ['saveScript', 'force', 'shutdown'],
133135
} as const satisfies CommandSchemaOverride;

src/commands/management/device.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ const bootCliSchema = {
5353
allowedFlags: ['headless'],
5454
} as const satisfies CommandSchemaOverride;
5555

56+
const devicesCliSchema = {
57+
helpDescription:
58+
'List available devices and simulators that can be selected for automation. Use platform, device, udid, or serial inputs on later commands to target one result.',
59+
} as const satisfies CommandSchemaOverride;
60+
5661
const capabilitiesCliSchema = {
5762
summary: 'List supported commands for the selected device',
5863
helpDescription:
@@ -79,6 +84,7 @@ const devicesCommandFacet = defineCommandFacet({
7984
name: 'devices',
8085
metadata: devicesCommandMetadata,
8186
definition: devicesCommandDefinition,
87+
cliSchema: devicesCliSchema,
8288
cliReader: commonCliReader,
8389
daemonWriter: devicesDaemonWriter,
8490
cliOutputFormatter: managementCliOutputFormatters.devices,

src/commands/management/install.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,14 @@ const installFromSourceCommandDefinition = defineExecutableCommand(
6868
const installCliSchema = {
6969
usageOverride: 'install <path> | install <app> <path>',
7070
listUsageOverride: 'install <path>',
71+
helpDescription:
72+
'Install an app binary from a local path. Provide an app identifier with the path when the target needs explicit app selection; use reinstall to replace an already installed app.',
7173
positionalArgs: ['appOrPath', 'path?'],
7274
} as const satisfies CommandSchemaOverride;
7375

7476
const reinstallCliSchema = {
77+
helpDescription:
78+
'Replace an installed app with a binary from a local path. Use this when preserving the same app identity while installing a new build on the selected device.',
7579
positionalArgs: ['app', 'path'],
7680
} as const satisfies CommandSchemaOverride;
7781

src/commands/react-native/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export const reactNativeCommandDefinition = defineExecutableCommand(
2828
const reactNativeCliSchema = {
2929
usageOverride: 'react-native dismiss-overlay',
3030
listUsageOverride: 'react-native dismiss-overlay',
31+
helpDescription:
32+
'Run supported React Native automation helpers. Use dismiss-overlay to close a visible development error overlay before continuing normal UI automation.',
3133
positionalArgs: ['dismiss-overlay'],
3234
} as const satisfies CommandSchemaOverride;
3335

src/commands/system/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,21 @@ const appStateCliSchema = {
169169

170170
const backCliSchema = {
171171
usageOverride: 'back [--in-app|--system]',
172+
helpDescription:
173+
'Navigate back in the app or through system navigation. Use in-app for the app navigation stack and system when the platform back behavior is required.',
172174
allowedFlags: ['backMode'],
173175
} as const satisfies CommandSchemaOverride;
174176

177+
const homeCliSchema = {
178+
helpDescription:
179+
'Send the selected device to its home screen. This leaves the app session open but moves the foreground away from the app.',
180+
} as const satisfies CommandSchemaOverride;
181+
182+
const appSwitcherCliSchema = {
183+
helpDescription:
184+
'Open the device app switcher to inspect or change foreground apps. This changes the visible system UI and may move focus away from the current app.',
185+
} as const satisfies CommandSchemaOverride;
186+
175187
const orientationCliSchema = {
176188
usageOverride: 'orientation <portrait|portrait-upside-down|landscape-left|landscape-right>',
177189
helpDescription: 'Set device orientation on iOS and Android',
@@ -283,6 +295,7 @@ const homeCommandFacet = defineCommandFacet({
283295
name: HOME_COMMAND_NAME,
284296
metadata: homeCommandMetadata,
285297
definition: homeCommandDefinition,
298+
cliSchema: homeCliSchema,
286299
cliReader: homeCliReader,
287300
daemonWriter: homeDaemonWriter,
288301
cliOutputFormatter: systemCliOutputFormatters.home,
@@ -302,6 +315,7 @@ const appSwitcherCommandFacet = defineCommandFacet({
302315
name: APP_SWITCHER_COMMAND_NAME,
303316
metadata: appSwitcherCommandMetadata,
304317
definition: appSwitcherCommandDefinition,
318+
cliSchema: appSwitcherCliSchema,
305319
cliReader: appSwitcherCliReader,
306320
daemonWriter: appSwitcherDaemonWriter,
307321
cliOutputFormatter: systemCliOutputFormatters['app-switcher'],

0 commit comments

Comments
 (0)