appium-mcp 1.71.0 → 1.71.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/command.d.ts.map +1 -1
  3. package/dist/command.js +14 -1
  4. package/dist/command.js.map +1 -1
  5. package/dist/devicemanager/adb-manager.d.ts +6 -6
  6. package/dist/devicemanager/adb-manager.d.ts.map +1 -1
  7. package/dist/devicemanager/adb-manager.js +15 -15
  8. package/dist/devicemanager/adb-manager.js.map +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/scripts/simple-index-documentation.js +1 -1
  12. package/dist/scripts/simple-index-documentation.js.map +1 -1
  13. package/dist/scripts/simple-query-documentation.js +1 -1
  14. package/dist/scripts/simple-query-documentation.js.map +1 -1
  15. package/dist/session-store.d.ts +16 -0
  16. package/dist/session-store.d.ts.map +1 -1
  17. package/dist/session-store.js +7 -3
  18. package/dist/session-store.js.map +1 -1
  19. package/dist/tests/session-store.test.js +0 -19
  20. package/dist/tests/session-store.test.js.map +1 -1
  21. package/dist/tests/test-setup-wda.js +1 -1
  22. package/dist/tests/test-setup-wda.js.map +1 -1
  23. package/dist/tools/documentation/reasoning-rag.d.ts +16 -16
  24. package/dist/tools/documentation/reasoning-rag.d.ts.map +1 -1
  25. package/dist/tools/documentation/reasoning-rag.js +100 -100
  26. package/dist/tools/documentation/reasoning-rag.js.map +1 -1
  27. package/dist/tools/documentation/sentence-transformers-embeddings.d.ts +8 -8
  28. package/dist/tools/documentation/sentence-transformers-embeddings.d.ts.map +1 -1
  29. package/dist/tools/documentation/sentence-transformers-embeddings.js +36 -36
  30. package/dist/tools/documentation/sentence-transformers-embeddings.js.map +1 -1
  31. package/dist/tools/interactions/keyboard.d.ts.map +1 -1
  32. package/dist/tools/interactions/keyboard.js +19 -19
  33. package/dist/tools/interactions/keyboard.js.map +1 -1
  34. package/dist/tools/session/attach-session.d.ts.map +1 -1
  35. package/dist/tools/session/attach-session.js +19 -19
  36. package/dist/tools/session/attach-session.js.map +1 -1
  37. package/package.json +1 -1
  38. package/server.json +2 -2
  39. package/src/command.ts +30 -0
  40. package/src/devicemanager/adb-manager.ts +18 -18
  41. package/src/index.ts +1 -1
  42. package/src/resources/submodules.zip +0 -0
  43. package/src/scripts/simple-index-documentation.ts +1 -1
  44. package/src/scripts/simple-query-documentation.ts +1 -1
  45. package/src/session-store.ts +8 -4
  46. package/src/tools/documentation/reasoning-rag.ts +153 -153
  47. package/src/tools/documentation/sentence-transformers-embeddings.ts +50 -50
  48. package/src/tools/interactions/keyboard.ts +26 -26
  49. package/src/tools/session/attach-session.ts +23 -23
@@ -92,6 +92,24 @@ export class ADBManager {
92
92
  }
93
93
  }
94
94
 
95
+ /**
96
+ * Get ADB instance for specific device operations
97
+ * This method ensures we reuse the singleton instance
98
+ * @param udid Optional device UDID for device-specific operations
99
+ * @returns Promise<ADB> The ADB instance
100
+ */
101
+ public async getADBForDevice(udid?: string): Promise<ADB> {
102
+ if (!this.isADBInitialized()) {
103
+ await this.initialize({ udid });
104
+ }
105
+
106
+ if (!this.adbInstance) {
107
+ throw new Error('ADB instance not available');
108
+ }
109
+
110
+ return this.adbInstance;
111
+ }
112
+
95
113
  /**
96
114
  * Create ADB instance with proper error handling
97
115
  * @param options ADB configuration options
@@ -118,24 +136,6 @@ export class ADBManager {
118
136
  throw new Error(`ADB initialization failed: ${error}`);
119
137
  }
120
138
  }
121
-
122
- /**
123
- * Get ADB instance for specific device operations
124
- * This method ensures we reuse the singleton instance
125
- * @param udid Optional device UDID for device-specific operations
126
- * @returns Promise<ADB> The ADB instance
127
- */
128
- public async getADBForDevice(udid?: string): Promise<ADB> {
129
- if (!this.isADBInitialized()) {
130
- await this.initialize({ udid });
131
- }
132
-
133
- if (!this.adbInstance) {
134
- throw new Error('ADB instance not available');
135
- }
136
-
137
- return this.adbInstance;
138
- }
139
139
  }
140
140
 
141
141
  /**
package/src/index.ts CHANGED
@@ -42,4 +42,4 @@ async function startServer(): Promise<void> {
42
42
  }
43
43
 
44
44
  // Start the server
45
- startServer();
45
+ void startServer();
Binary file
@@ -90,4 +90,4 @@ async function main(): Promise<void> {
90
90
  }
91
91
  }
92
92
 
93
- main();
93
+ void main();
@@ -58,4 +58,4 @@ async function main(): Promise<void> {
58
58
  }
59
59
  }
60
60
 
61
- main();
61
+ void main();
@@ -109,10 +109,7 @@ export function setSession(
109
109
  // `getAppiumSessionCapabilities()` may return metadata fields without the
110
110
  // `appium:` prefix, so accept both namespaced and non-namespaced variants.
111
111
  const metadata: SessionMetadata = {
112
- platform:
113
- (capabilities.platformName as string | undefined) ??
114
- (capabilities['appium:platformName'] as string | undefined) ??
115
- null,
112
+ platform: (capabilities.platformName as string | undefined) ?? null,
116
113
  automationName:
117
114
  (capabilities['appium:automationName'] as string | undefined) ??
118
115
  (capabilities.automationName as string | undefined) ??
@@ -215,6 +212,13 @@ export function setCurrentContext(
215
212
  return true;
216
213
  }
217
214
 
215
+ export function getSessionInfo(sessionId: string | null): SessionInfo | null {
216
+ if (!sessionId) {
217
+ return null;
218
+ }
219
+ return sessions.get(sessionId) ?? null;
220
+ }
221
+
218
222
  export function getCurrentContext(sessionId?: string): string | null {
219
223
  const id = sessionId ?? activeSessionId;
220
224
  if (!id) {
@@ -60,7 +60,159 @@ export class ReasoningRAG {
60
60
  private isInitialized: boolean = false;
61
61
 
62
62
  constructor() {
63
- this.initializeTransformers();
63
+ void this.initializeTransformers();
64
+ }
65
+
66
+ /**
67
+ * Enhanced RAG query with reasoning capabilities
68
+ */
69
+ async queryWithReasoning(
70
+ query: string,
71
+ options: {
72
+ topK?: number;
73
+ reasoningTasks?: ReasoningTask[];
74
+ customConfigs?: ReasoningConfig[];
75
+ } = {}
76
+ ): Promise<EnhancedRAGResponse> {
77
+ const {
78
+ topK = 50,
79
+ reasoningTasks = ['summarization', 'question-answering'],
80
+ customConfigs,
81
+ } = options;
82
+
83
+ try {
84
+ log.info(`Starting reasoning-enhanced RAG query: "${query}"`);
85
+
86
+ // Step 1: Retrieve relevant chunks using existing RAG
87
+ log.info(`Retrieving top ${topK} relevant chunks...`);
88
+ const retrievedChunks = await queryVectorStore(query, topK);
89
+
90
+ if (!retrievedChunks || retrievedChunks.length === 0) {
91
+ return {
92
+ query,
93
+ retrievedChunks: [],
94
+ reasoningResults: [],
95
+ summary: 'No relevant information found in the documentation.',
96
+ answer: 'No relevant information found to answer your query.',
97
+ sources: [],
98
+ };
99
+ }
100
+
101
+ log.info(`Retrieved ${retrievedChunks.length} chunks for reasoning`);
102
+
103
+ // Step 2: Configure reasoning models
104
+ const configs: ReasoningConfig[] = customConfigs || [
105
+ // Summarization using T5
106
+ {
107
+ task: 'summarization',
108
+ modelName: 'Xenova/t5-small',
109
+ maxLength: 150,
110
+ minLength: 30,
111
+ },
112
+ // Question answering using DistilBERT
113
+ {
114
+ task: 'question-answering',
115
+ modelName: 'Xenova/distilbert-base-cased-distilled-squad',
116
+ },
117
+ ];
118
+
119
+ // Filter configs based on requested tasks
120
+ const filteredConfigs = configs.filter((config) =>
121
+ reasoningTasks.includes(config.task)
122
+ );
123
+
124
+ // Step 3: Perform reasoning on retrieved chunks
125
+ log.info(
126
+ `Performing reasoning with ${filteredConfigs.length} different models...`
127
+ );
128
+ const reasoningResults = await this.processChunksWithReasoning(
129
+ retrievedChunks,
130
+ filteredConfigs,
131
+ query
132
+ );
133
+
134
+ // Step 4: Generate comprehensive summary
135
+ log.info('Generating comprehensive summary...');
136
+ const summary = await this.generateComprehensiveSummary(
137
+ reasoningResults,
138
+ query
139
+ );
140
+
141
+ // Step 5: Extract best answer from reasoning results
142
+ const qaResults = reasoningResults.filter(
143
+ (result) =>
144
+ result.metadata.task === 'question-answering' &&
145
+ !result.metadata.error
146
+ );
147
+
148
+ const bestAnswer =
149
+ qaResults.length > 0
150
+ ? qaResults.sort(
151
+ (a, b) => (b.confidence || 0) - (a.confidence || 0)
152
+ )[0].reasoningOutput
153
+ : summary;
154
+
155
+ // Step 6: Extract sources
156
+ const sources = retrievedChunks
157
+ .map(
158
+ (doc: any) =>
159
+ doc.metadata?.relativePath ||
160
+ doc.metadata?.filename ||
161
+ doc.metadata?.source
162
+ )
163
+ .filter(
164
+ (source: any, index: number, arr: any[]) =>
165
+ source && arr.indexOf(source) === index
166
+ );
167
+
168
+ log.info(
169
+ `Reasoning-enhanced RAG completed. Generated ${reasoningResults.length} reasoning results from ${sources.length} sources`
170
+ );
171
+
172
+ return {
173
+ query,
174
+ retrievedChunks,
175
+ reasoningResults,
176
+ summary,
177
+ answer: bestAnswer,
178
+ sources,
179
+ };
180
+ } catch (error) {
181
+ log.error('Error in reasoning-enhanced RAG:', error);
182
+ throw new Error(
183
+ `Reasoning-enhanced RAG failed: ${error instanceof Error ? error.message : String(error)}`
184
+ );
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Get available reasoning models and their capabilities
190
+ */
191
+ getAvailableModels(): Record<ReasoningTask, string[]> {
192
+ return {
193
+ summarization: [
194
+ 'Xenova/t5-small',
195
+ 'Xenova/t5-base',
196
+ 'Xenova/bart-large-cnn',
197
+ ],
198
+ 'question-answering': [
199
+ 'Xenova/distilbert-base-cased-distilled-squad',
200
+ 'Xenova/roberta-base-squad2',
201
+ ],
202
+ analysis: ['Xenova/gpt2', 'Xenova/distilgpt2'],
203
+ classification: [
204
+ 'Xenova/distilbert-base-uncased-finetuned-sst-2-english',
205
+ 'Xenova/bert-base-uncased',
206
+ ],
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Clean up resources
212
+ */
213
+ async cleanup(): Promise<void> {
214
+ this.models.clear();
215
+ log.info('Reasoning RAG resources cleaned up');
64
216
  }
65
217
 
66
218
  /**
@@ -277,158 +429,6 @@ export class ReasoningRAG {
277
429
 
278
430
  return comprehensiveSummary;
279
431
  }
280
-
281
- /**
282
- * Enhanced RAG query with reasoning capabilities
283
- */
284
- async queryWithReasoning(
285
- query: string,
286
- options: {
287
- topK?: number;
288
- reasoningTasks?: ReasoningTask[];
289
- customConfigs?: ReasoningConfig[];
290
- } = {}
291
- ): Promise<EnhancedRAGResponse> {
292
- const {
293
- topK = 50,
294
- reasoningTasks = ['summarization', 'question-answering'],
295
- customConfigs,
296
- } = options;
297
-
298
- try {
299
- log.info(`Starting reasoning-enhanced RAG query: "${query}"`);
300
-
301
- // Step 1: Retrieve relevant chunks using existing RAG
302
- log.info(`Retrieving top ${topK} relevant chunks...`);
303
- const retrievedChunks = await queryVectorStore(query, topK);
304
-
305
- if (!retrievedChunks || retrievedChunks.length === 0) {
306
- return {
307
- query,
308
- retrievedChunks: [],
309
- reasoningResults: [],
310
- summary: 'No relevant information found in the documentation.',
311
- answer: 'No relevant information found to answer your query.',
312
- sources: [],
313
- };
314
- }
315
-
316
- log.info(`Retrieved ${retrievedChunks.length} chunks for reasoning`);
317
-
318
- // Step 2: Configure reasoning models
319
- const configs: ReasoningConfig[] = customConfigs || [
320
- // Summarization using T5
321
- {
322
- task: 'summarization',
323
- modelName: 'Xenova/t5-small',
324
- maxLength: 150,
325
- minLength: 30,
326
- },
327
- // Question answering using DistilBERT
328
- {
329
- task: 'question-answering',
330
- modelName: 'Xenova/distilbert-base-cased-distilled-squad',
331
- },
332
- ];
333
-
334
- // Filter configs based on requested tasks
335
- const filteredConfigs = configs.filter((config) =>
336
- reasoningTasks.includes(config.task)
337
- );
338
-
339
- // Step 3: Perform reasoning on retrieved chunks
340
- log.info(
341
- `Performing reasoning with ${filteredConfigs.length} different models...`
342
- );
343
- const reasoningResults = await this.processChunksWithReasoning(
344
- retrievedChunks,
345
- filteredConfigs,
346
- query
347
- );
348
-
349
- // Step 4: Generate comprehensive summary
350
- log.info('Generating comprehensive summary...');
351
- const summary = await this.generateComprehensiveSummary(
352
- reasoningResults,
353
- query
354
- );
355
-
356
- // Step 5: Extract best answer from reasoning results
357
- const qaResults = reasoningResults.filter(
358
- (result) =>
359
- result.metadata.task === 'question-answering' &&
360
- !result.metadata.error
361
- );
362
-
363
- const bestAnswer =
364
- qaResults.length > 0
365
- ? qaResults.sort(
366
- (a, b) => (b.confidence || 0) - (a.confidence || 0)
367
- )[0].reasoningOutput
368
- : summary;
369
-
370
- // Step 6: Extract sources
371
- const sources = retrievedChunks
372
- .map(
373
- (doc: any) =>
374
- doc.metadata?.relativePath ||
375
- doc.metadata?.filename ||
376
- doc.metadata?.source
377
- )
378
- .filter(
379
- (source: any, index: number, arr: any[]) =>
380
- source && arr.indexOf(source) === index
381
- );
382
-
383
- log.info(
384
- `Reasoning-enhanced RAG completed. Generated ${reasoningResults.length} reasoning results from ${sources.length} sources`
385
- );
386
-
387
- return {
388
- query,
389
- retrievedChunks,
390
- reasoningResults,
391
- summary,
392
- answer: bestAnswer,
393
- sources,
394
- };
395
- } catch (error) {
396
- log.error('Error in reasoning-enhanced RAG:', error);
397
- throw new Error(
398
- `Reasoning-enhanced RAG failed: ${error instanceof Error ? error.message : String(error)}`
399
- );
400
- }
401
- }
402
-
403
- /**
404
- * Get available reasoning models and their capabilities
405
- */
406
- getAvailableModels(): Record<ReasoningTask, string[]> {
407
- return {
408
- summarization: [
409
- 'Xenova/t5-small',
410
- 'Xenova/t5-base',
411
- 'Xenova/bart-large-cnn',
412
- ],
413
- 'question-answering': [
414
- 'Xenova/distilbert-base-cased-distilled-squad',
415
- 'Xenova/roberta-base-squad2',
416
- ],
417
- analysis: ['Xenova/gpt2', 'Xenova/distilgpt2'],
418
- classification: [
419
- 'Xenova/distilbert-base-uncased-finetuned-sst-2-english',
420
- 'Xenova/bert-base-uncased',
421
- ],
422
- };
423
- }
424
-
425
- /**
426
- * Clean up resources
427
- */
428
- async cleanup(): Promise<void> {
429
- this.models.clear();
430
- log.info('Reasoning RAG resources cleaned up');
431
- }
432
432
  }
433
433
 
434
434
  // Export a singleton instance
@@ -21,56 +21,6 @@ export class SentenceTransformersEmbeddings {
21
21
  this.modelName = options.modelName || 'Xenova/all-MiniLM-L6-v2';
22
22
  }
23
23
 
24
- /**
25
- * Initialize the transformers library dynamically
26
- */
27
- private async initializeTransformers(): Promise<void> {
28
- if (this.transformers) {
29
- return;
30
- }
31
-
32
- try {
33
- // Use eval to avoid CommonJS/ESM conflict during compilation
34
- const importTransformers = new Function(
35
- 'return import("@xenova/transformers")'
36
- );
37
- this.transformers = await importTransformers();
38
- } catch (error) {
39
- log.error('Error importing @xenova/transformers:', error);
40
- throw new Error(
41
- `Failed to import @xenova/transformers: ${error instanceof Error ? error.message : String(error)}`
42
- );
43
- }
44
- }
45
-
46
- /**
47
- * Initialize the model lazily
48
- */
49
- private async initializeModel(): Promise<void> {
50
- if (this.isInitialized && this.model) {
51
- return;
52
- }
53
-
54
- await this.initializeTransformers();
55
-
56
- log.info(`Initializing sentence-transformers model: ${this.modelName}`);
57
- try {
58
- this.model = await this.transformers.pipeline(
59
- 'feature-extraction',
60
- this.modelName
61
- );
62
- this.isInitialized = true;
63
- log.info(
64
- `Successfully initialized sentence-transformers model: ${this.modelName}`
65
- );
66
- } catch (error) {
67
- log.error('Error initializing sentence-transformers model:', error);
68
- throw new Error(
69
- `Failed to initialize sentence-transformers model: ${error instanceof Error ? error.message : String(error)}`
70
- );
71
- }
72
- }
73
-
74
24
  /**
75
25
  * Generate embeddings for a single text (LangChain interface)
76
26
  */
@@ -141,4 +91,54 @@ export class SentenceTransformersEmbeddings {
141
91
  );
142
92
  }
143
93
  }
94
+
95
+ /**
96
+ * Initialize the transformers library dynamically
97
+ */
98
+ private async initializeTransformers(): Promise<void> {
99
+ if (this.transformers) {
100
+ return;
101
+ }
102
+
103
+ try {
104
+ // Use eval to avoid CommonJS/ESM conflict during compilation
105
+ const importTransformers = new Function(
106
+ 'return import("@xenova/transformers")'
107
+ );
108
+ this.transformers = await importTransformers();
109
+ } catch (error) {
110
+ log.error('Error importing @xenova/transformers:', error);
111
+ throw new Error(
112
+ `Failed to import @xenova/transformers: ${error instanceof Error ? error.message : String(error)}`
113
+ );
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Initialize the model lazily
119
+ */
120
+ private async initializeModel(): Promise<void> {
121
+ if (this.isInitialized && this.model) {
122
+ return;
123
+ }
124
+
125
+ await this.initializeTransformers();
126
+
127
+ log.info(`Initializing sentence-transformers model: ${this.modelName}`);
128
+ try {
129
+ this.model = await this.transformers.pipeline(
130
+ 'feature-extraction',
131
+ this.modelName
132
+ );
133
+ this.isInitialized = true;
134
+ log.info(
135
+ `Successfully initialized sentence-transformers model: ${this.modelName}`
136
+ );
137
+ } catch (error) {
138
+ log.error('Error initializing sentence-transformers model:', error);
139
+ throw new Error(
140
+ `Failed to initialize sentence-transformers model: ${error instanceof Error ? error.message : String(error)}`
141
+ );
142
+ }
143
+ }
144
144
  }
@@ -30,32 +30,6 @@ const schema = z.object({
30
30
 
31
31
  type KeyboardArgs = z.infer<typeof schema>;
32
32
 
33
- async function handleHide(
34
- sessionId: string | undefined,
35
- keys: string[] | undefined
36
- ): Promise<ContentResult> {
37
- const resolved = resolveDriver(sessionId);
38
- if (!resolved.ok) {
39
- return resolved.result;
40
- }
41
- const { driver } = resolved;
42
-
43
- const params = keys && keys.length > 0 ? { keys } : {};
44
- await execute(driver, 'mobile: hideKeyboard', params);
45
- return textResult('Keyboard dismissed successfully.');
46
- }
47
-
48
- async function handleIsShown(sessionId?: string): Promise<ContentResult> {
49
- const resolved = resolveDriver(sessionId);
50
- if (!resolved.ok) {
51
- return resolved.result;
52
- }
53
- const { driver } = resolved;
54
-
55
- const keyboardShown = await execute(driver, 'mobile: isKeyboardShown', {});
56
- return textResult(JSON.stringify({ keyboardShown }, null, 2));
57
- }
58
-
59
33
  export default function keyboard(server: FastMCP): void {
60
34
  server.addTool({
61
35
  name: 'appium_mobile_keyboard',
@@ -90,3 +64,29 @@ export default function keyboard(server: FastMCP): void {
90
64
  },
91
65
  });
92
66
  }
67
+
68
+ async function handleHide(
69
+ sessionId: string | undefined,
70
+ keys: string[] | undefined
71
+ ): Promise<ContentResult> {
72
+ const resolved = resolveDriver(sessionId);
73
+ if (!resolved.ok) {
74
+ return resolved.result;
75
+ }
76
+ const { driver } = resolved;
77
+
78
+ const params = keys && keys.length > 0 ? { keys } : {};
79
+ await execute(driver, 'mobile: hideKeyboard', params);
80
+ return textResult('Keyboard dismissed successfully.');
81
+ }
82
+
83
+ async function handleIsShown(sessionId?: string): Promise<ContentResult> {
84
+ const resolved = resolveDriver(sessionId);
85
+ if (!resolved.ok) {
86
+ return resolved.result;
87
+ }
88
+ const { driver } = resolved;
89
+
90
+ const keyboardShown = await execute(driver, 'mobile: isKeyboardShown', {});
91
+ return textResult(JSON.stringify({ keyboardShown }, null, 2));
92
+ }
@@ -38,29 +38,6 @@ const METADATA_FIELDS = [
38
38
  ['deviceName', 'appium:deviceName', 'appium:deviceName'],
39
39
  ] as const;
40
40
 
41
- /**
42
- * Read capabilities from a WebdriverIO client method when available.
43
- *
44
- * @param client - Attached WebdriverIO client for the target Appium session.
45
- * @param methodName - Capability reader to invoke on the client.
46
- * @returns Parsed capabilities, or `undefined` when the method is missing or fails.
47
- */
48
- async function readClientCapabilities(
49
- client: Client,
50
- methodName: 'getAppiumSessionCapabilities' | 'getSession'
51
- ): Promise<SessionCapabilities | undefined> {
52
- const method = client[methodName];
53
- if (typeof method !== 'function') {
54
- return undefined;
55
- }
56
-
57
- try {
58
- return readCapabilities(await method.call(client));
59
- } catch {
60
- return undefined;
61
- }
62
- }
63
-
64
41
  /**
65
42
  * Attach MCP Appium to an existing remote Appium session without taking
66
43
  * ownership of the underlying session lifecycle.
@@ -154,3 +131,26 @@ export async function attachSessionAction(args: {
154
131
  );
155
132
  }
156
133
  }
134
+
135
+ /**
136
+ * Read capabilities from a WebdriverIO client method when available.
137
+ *
138
+ * @param client - Attached WebdriverIO client for the target Appium session.
139
+ * @param methodName - Capability reader to invoke on the client.
140
+ * @returns Parsed capabilities, or `undefined` when the method is missing or fails.
141
+ */
142
+ async function readClientCapabilities(
143
+ client: Client,
144
+ methodName: 'getAppiumSessionCapabilities' | 'getSession'
145
+ ): Promise<SessionCapabilities | undefined> {
146
+ const method = client[methodName];
147
+ if (typeof method !== 'function') {
148
+ return undefined;
149
+ }
150
+
151
+ try {
152
+ return readCapabilities(await method.call(client));
153
+ } catch {
154
+ return undefined;
155
+ }
156
+ }