Skip to content

Commit 6f94d4d

Browse files
committed
Add limit and execution time feature, reformat description
1 parent 1d0d647 commit 6f94d4d

File tree

1 file changed

+113
-11
lines changed

1 file changed

+113
-11
lines changed

src/server.ts

Lines changed: 113 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
88
import * as sdkTypes from '@modelcontextprotocol/sdk/types.js';
99
// import { ZodError } from 'zod'; // ZodError is not directly used from here, handled by SDK or refined errors
1010
import { Logger } from './logger.js';
11-
import { ExecuteScriptInputSchema, GetScriptingTipsInputSchema, AXQueryInputSchema } from './schemas.js';
11+
import { ExecuteScriptInputSchema, GetScriptingTipsInputSchema, AXQueryInputSchema, type AXQueryInput } from './schemas.js';
1212
import { ScriptExecutor } from './ScriptExecutor.js';
1313
import { AXQueryExecutor } from './AXQueryExecutor.js';
1414
import type { ScriptExecutionError, ExecuteScriptResponse } from './types.js';
@@ -74,17 +74,19 @@ const GetScriptingTipsInputShape = {
7474
} as const;
7575

7676
const AXQueryInputShape = {
77-
cmd: z.enum(['query', 'perform']),
78-
multi: z.boolean().optional(),
77+
command: z.enum(['query', 'perform']),
78+
return_all_matches: z.boolean().optional(),
7979
locator: z.object({
8080
app: z.string(),
8181
role: z.string(),
8282
match: z.record(z.string()),
83-
pathHint: z.array(z.string()).optional(),
83+
navigation_path_hint: z.array(z.string()).optional(),
8484
}),
85-
attributes: z.array(z.string()).optional(),
86-
requireAction: z.string().optional(),
87-
action: z.string().optional(),
85+
attributes_to_query: z.array(z.string()).optional(),
86+
required_action_name: z.string().optional(),
87+
action_to_perform: z.string().optional(),
88+
report_execution_time: z.boolean().optional().default(false),
89+
limit: z.number().int().positive().optional().default(500),
8890
} as const;
8991

9092
async function main() {
@@ -415,19 +417,90 @@ async function main() {
415417
// ADD THE NEW accessibility_query TOOL HERE
416418
server.tool(
417419
'accessibility_query',
418-
'Query and interact with the macOS accessibility interface to inspect UI elements of applications. This tool provides a powerful way to explore and manipulate the user interface elements of any application using the native macOS accessibility framework.\\n\\nThis tool exposes the complete macOS accessibility API capabilities, allowing detailed inspection of UI elements and their properties. It\'s particularly useful for automating interactions with applications that don\'t have robust AppleScript support or when you need to inspect the UI structure in detail.\\n\\n**Input Parameters:**\\n\\n* `cmd` (enum: \'query\' | \'perform\', required): The operation to perform.\\n * `query`: Retrieves information about UI elements.\\n * `perform`: Executes an action on a UI element (like clicking a button).\\n\\n* `locator` (object, required): Specifications to find the target element(s).\\n * `app` (string, required): The application to target, specified by either bundle ID or display name (e.g., "Safari", "com.apple.Safari").\\n * `role` (string, required): The accessibility role of the target element (e.g., "AXButton", "AXStaticText").\\n * `match` (object, required): Key-value pairs of attributes to match. Can be empty ({}) if not needed.\\n * `pathHint` (array of strings, optional): Path to navigate within the application hierarchy (e.g., ["window[1]", "toolbar[1]"]).\\n\\n* `multi` (boolean, optional): When `true`, returns all matching elements rather than just the first match. Default is `false`.\\n\\n* `attributes` (array of strings, optional): Specific attributes to query for matched elements. If not provided, common attributes will be included. Examples: ["AXRole", "AXTitle", "AXValue"]\\n\\n* `requireAction` (string, optional): Filter elements to only those supporting a specific action (e.g., "AXPress" for clickable elements).\\n\\n* `action` (string, optional, required when cmd="perform"): The accessibility action to perform on the matched element (e.g., "AXPress" to click a button).\\n\\n**Example Queries:**\\n\\n1. Find all text elements in the front Safari window:\\n```json\\n{\\n "cmd": "query",\\n "multi": true,\\n "locator": {\\n "app": "Safari",\\n "role": "AXStaticText",\\n "match": {},\\n "pathHint": ["window[1]"]\\n }\\n}\\n```\\n\\n2. Find and click a button with a specific title:\\n```json\\n{\\n "cmd": "perform",\\n "locator": {\\n "app": "System Settings",\\n "role": "AXButton",\\n "match": {"AXTitle": "General"}\\n },\\n "action": "AXPress"\\n}\\n```\\n\\n3. Get detailed information about the focused UI element:\\n```json\\n{\\n "cmd": "query",\\n "locator": {\\n "app": "Mail",\\n "role": "AXTextField",\\n "match": {"AXFocused": "true"}\\n },\\n "attributes": ["AXRole", "AXTitle", "AXValue", "AXDescription", "AXHelp", "AXPosition", "AXSize"]\\n}\\n```\\n\\n**Note:** Using this tool requires that the application running this server has the necessary Accessibility permissions in macOS System Settings > Privacy & Security > Accessibility.',
420+
`Query and interact with the macOS accessibility interface to inspect UI elements of applications. This tool provides a powerful way to explore and manipulate the user interface elements of any application using the native macOS accessibility framework.
421+
422+
This tool exposes the complete macOS accessibility API capabilities, allowing detailed inspection of UI elements and their properties. It's particularly useful for automating interactions with applications that don't have robust AppleScript support or when you need to inspect the UI structure in detail.
423+
424+
**Input Parameters:**
425+
426+
* \`command\` (enum: 'query' | 'perform', required): The operation to perform.
427+
* \`query\`: Retrieves information about UI elements.
428+
* \`perform\`: Executes an action on a UI element (like clicking a button).
429+
430+
* \`locator\` (object, required): Specifications to find the target element(s).
431+
* \`app\` (string, required): The application to target, specified by either bundle ID or display name (e.g., "Safari", "com.apple.Safari").
432+
* \`role\` (string, required): The accessibility role of the target element (e.g., "AXButton", "AXStaticText").
433+
* \`match\` (object, required): Key-value pairs of attributes to match. Can be empty (\`{}\`) if not needed.
434+
* \`navigation_path_hint\` (array of strings, optional): Path to navigate within the application hierarchy (e.g., \`["window[1]", "toolbar[1]"]\`).
435+
436+
* \`return_all_matches\` (boolean, optional): When \`true\`, returns all matching elements rather than just the first match. Default is \`false\`.
437+
438+
* \`attributes_to_query\` (array of strings, optional): Specific attributes to query for matched elements. If not provided, common attributes will be included. Examples: \`["AXRole", "AXTitle", "AXValue"]\`
439+
440+
* \`required_action_name\` (string, optional): Filter elements to only those supporting a specific action (e.g., "AXPress" for clickable elements).
441+
442+
* \`action_to_perform\` (string, optional, required when \`command="perform"\`): The accessibility action to perform on the matched element (e.g., "AXPress" to click a button).
443+
444+
* \`report_execution_time\` (boolean, optional): If true, the tool will return an additional message containing the formatted script execution time. Defaults to false.
445+
446+
* \`limit\` (integer, optional): Maximum number of lines to return in the output. Defaults to 500. Output will be truncated if it exceeds this limit.
447+
448+
**Example Queries (Note: key names have changed to snake_case):**
449+
450+
1. **Find all text elements in the front Safari window:**
451+
\`\`\`json
452+
{
453+
"command": "query",
454+
"return_all_matches": true,
455+
"locator": {
456+
"app": "Safari",
457+
"role": "AXStaticText",
458+
"match": {},
459+
"navigation_path_hint": ["window[1]"]
460+
}
461+
}
462+
\`\`\`
463+
464+
2. **Find and click a button with a specific title:**
465+
\`\`\`json
466+
{
467+
"command": "perform",
468+
"locator": {
469+
"app": "System Settings",
470+
"role": "AXButton",
471+
"match": {"AXTitle": "General"}
472+
},
473+
"action_to_perform": "AXPress"
474+
}
475+
\`\`\`
476+
477+
3. **Get detailed information about the focused UI element:**
478+
\`\`\`json
479+
{
480+
"command": "query",
481+
"locator": {
482+
"app": "Mail",
483+
"role": "AXTextField",
484+
"match": {"AXFocused": "true"}
485+
},
486+
"attributes_to_query": ["AXRole", "AXTitle", "AXValue", "AXDescription", "AXHelp", "AXPosition", "AXSize"]
487+
}
488+
\`\`\`
489+
490+
**Note:** Using this tool requires that the application running this server has the necessary Accessibility permissions in macOS System Settings > Privacy & Security > Accessibility.`,
419491
AXQueryInputShape,
420492
async (args: unknown) => {
493+
let input: AXQueryInput; // Declare input here to make it accessible in catch
421494
try {
422-
const input = AXQueryInputSchema.parse(args);
495+
input = AXQueryInputSchema.parse(args);
423496
logger.info('accessibility_query called with input:', input);
424497

425498
const result = await axQueryExecutor.execute(input);
426499

427500
// For cleaner output, especially for multi-element queries, format the response
428501
let formattedOutput: string;
429502

430-
if (input.cmd === 'query' && input.multi === true) {
503+
if (input.command === 'query' && input.return_all_matches === true) {
431504
// For multi-element queries, format the results more readably
432505
if ('elements' in result) {
433506
formattedOutput = JSON.stringify(result, null, 2);
@@ -439,7 +512,36 @@ async function main() {
439512
formattedOutput = JSON.stringify(result, null, 2);
440513
}
441514

442-
return { content: [{ type: 'text', text: formattedOutput }] };
515+
// Apply line limit
516+
let finalOutputText = formattedOutput;
517+
const lines = finalOutputText.split('\n');
518+
if (input.limit !== undefined && lines.length > input.limit) {
519+
finalOutputText = lines.slice(0, input.limit).join('\n');
520+
const truncationNotice = `\n\n--- Output truncated to ${input.limit} lines. Original length was ${lines.length} lines. ---`;
521+
finalOutputText += truncationNotice;
522+
}
523+
524+
const responseContent: Array<{ type: 'text'; text: string }> = [{ type: 'text', text: finalOutputText }];
525+
526+
if (input.report_execution_time) {
527+
const ms = result.execution_time_seconds * 1000;
528+
let timeMessage = "Script executed in ";
529+
if (ms < 1) { // Less than 1 millisecond
530+
timeMessage += "<1 millisecond.";
531+
} else if (ms < 1000) { // 1ms up to 999ms
532+
timeMessage += `${ms.toFixed(0)} milliseconds.`;
533+
} else if (ms < 60000) { // 1 second up to 59.999 seconds
534+
timeMessage += `${(ms / 1000).toFixed(2)} seconds.`;
535+
} else {
536+
const totalSeconds = ms / 1000;
537+
const minutes = Math.floor(totalSeconds / 60);
538+
const remainingSeconds = Math.round(totalSeconds % 60);
539+
timeMessage += `${minutes} minute(s) and ${remainingSeconds} seconds.`;
540+
}
541+
responseContent.push({ type: 'text', text: `${timeMessage}` });
542+
}
543+
544+
return { content: responseContent };
443545
} catch (error: unknown) {
444546
const err = error as Error;
445547
logger.error('Error in accessibility_query tool handler', { message: err.message });

0 commit comments

Comments
 (0)