mcp-android-emulator 1.2.3 → 1.3.0

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 (3) hide show
  1. package/dist/index.js +219 -7
  2. package/package.json +1 -1
  3. package/src/index.ts +252 -7
package/dist/index.js CHANGED
@@ -41,7 +41,7 @@ async function shell(command) {
41
41
  // Create MCP server
42
42
  const server = new McpServer({
43
43
  name: "android-emulator",
44
- version: "1.2.3",
44
+ version: "1.3.0",
45
45
  });
46
46
  // =====================================================
47
47
  // TOOL: screenshot
@@ -398,12 +398,47 @@ server.tool("force_stop", "Force stop an application", {
398
398
  // TOOL: get_current_activity
399
399
  // =====================================================
400
400
  server.tool("get_current_activity", "Get the currently focused activity/screen", {}, async () => {
401
- const result = await shell("dumpsys activity activities | grep mResumedActivity");
401
+ let activity = "Unknown";
402
+ // Try multiple methods for compatibility across emulators
403
+ try {
404
+ // Method 1: mResumedActivity (standard Android)
405
+ const result1 = await shell("dumpsys activity activities | grep -E 'mResumedActivity|mCurrentFocus' || true");
406
+ if (result1 && result1.trim()) {
407
+ activity = result1.trim();
408
+ }
409
+ }
410
+ catch {
411
+ // Ignore
412
+ }
413
+ if (activity === "Unknown") {
414
+ try {
415
+ // Method 2: topActivity (alternative)
416
+ const result2 = await shell("dumpsys activity top | head -5 || true");
417
+ if (result2 && result2.trim()) {
418
+ activity = result2.trim();
419
+ }
420
+ }
421
+ catch {
422
+ // Ignore
423
+ }
424
+ }
425
+ if (activity === "Unknown") {
426
+ try {
427
+ // Method 3: window focus (Redroid/Docker compatible)
428
+ const result3 = await shell("dumpsys window | grep -E 'mCurrentFocus|mFocusedApp' || true");
429
+ if (result3 && result3.trim()) {
430
+ activity = result3.trim();
431
+ }
432
+ }
433
+ catch {
434
+ // Ignore
435
+ }
436
+ }
402
437
  return {
403
438
  content: [
404
439
  {
405
440
  type: "text",
406
- text: `Current activity: ${result.trim()}`,
441
+ text: `Current activity:\n${activity}`,
407
442
  },
408
443
  ],
409
444
  };
@@ -777,24 +812,44 @@ server.tool("scroll_to_text", "Scroll the screen until an element with specific
777
812
  // =====================================================
778
813
  // TOOL: wait_for_ui_stable
779
814
  // =====================================================
815
+ /**
816
+ * Extract a normalized fingerprint of UI elements from XML
817
+ * Only considers text, bounds, and class - ignores dynamic attributes
818
+ */
819
+ function extractUIFingerprint(xml) {
820
+ const elements = [];
821
+ // Match elements with text or class and bounds
822
+ const regex = /(?:text="([^"]*)")?[^>]*(?:class="([^"]*)")?[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g;
823
+ let match;
824
+ while ((match = regex.exec(xml)) !== null) {
825
+ const [, text, className, x1, y1, x2, y2] = match;
826
+ // Only include elements with text or meaningful classes
827
+ if (text || className) {
828
+ elements.push(`${text || ""}|${className || ""}|${x1},${y1},${x2},${y2}`);
829
+ }
830
+ }
831
+ return elements.sort().join("\n");
832
+ }
780
833
  server.tool("wait_for_ui_stable", "Wait for the UI to stop changing (useful after animations)", {
781
834
  timeout: z.number().optional().describe("Timeout in milliseconds (default: 5000)"),
782
835
  checkInterval: z.number().optional().describe("Check interval in milliseconds (default: 500)"),
783
836
  }, async ({ timeout = 5000, checkInterval = 500 }) => {
784
837
  const startTime = Date.now();
785
- let lastXml = "";
838
+ let lastFingerprint = "";
786
839
  let stableCount = 0;
787
840
  while (Date.now() - startTime < timeout) {
788
841
  await shell("uiautomator dump /sdcard/ui_dump.xml");
789
842
  const currentXml = await shell("cat /sdcard/ui_dump.xml");
790
- if (currentXml === lastXml) {
843
+ const currentFingerprint = extractUIFingerprint(currentXml);
844
+ if (currentFingerprint === lastFingerprint) {
791
845
  stableCount++;
792
846
  if (stableCount >= 2) {
847
+ const elapsed = Date.now() - startTime;
793
848
  return {
794
849
  content: [
795
850
  {
796
851
  type: "text",
797
- text: `UI stable after ${Math.round((Date.now() - startTime) / 1000)}s`,
852
+ text: `UI stable after ${elapsed < 1000 ? elapsed + "ms" : Math.round(elapsed / 1000) + "s"}`,
798
853
  },
799
854
  ],
800
855
  };
@@ -802,7 +857,7 @@ server.tool("wait_for_ui_stable", "Wait for the UI to stop changing (useful afte
802
857
  }
803
858
  else {
804
859
  stableCount = 0;
805
- lastXml = currentXml;
860
+ lastFingerprint = currentFingerprint;
806
861
  }
807
862
  await new Promise((resolve) => setTimeout(resolve, checkInterval));
808
863
  }
@@ -1217,6 +1272,163 @@ server.tool("assert_screen_contains", "Assert that specific text is visible on s
1217
1272
  ],
1218
1273
  };
1219
1274
  });
1275
+ // =====================================================
1276
+ // TOOL: get_all_text
1277
+ // =====================================================
1278
+ server.tool("get_all_text", "Get all visible text elements on screen (useful for debugging and verification)", {
1279
+ includeEmpty: z.boolean().optional().describe("Include elements with empty text (default: false)"),
1280
+ }, async ({ includeEmpty = false }) => {
1281
+ await shell("uiautomator dump /sdcard/ui_dump.xml");
1282
+ const xml = await shell("cat /sdcard/ui_dump.xml");
1283
+ const texts = [];
1284
+ const regex = /text="([^"]*)"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g;
1285
+ let match;
1286
+ while ((match = regex.exec(xml)) !== null) {
1287
+ const [, text, x1, y1, x2, y2] = match;
1288
+ if (text || includeEmpty) {
1289
+ texts.push({
1290
+ text: text || "(empty)",
1291
+ centerX: Math.round((parseInt(x1) + parseInt(x2)) / 2),
1292
+ centerY: Math.round((parseInt(y1) + parseInt(y2)) / 2),
1293
+ });
1294
+ }
1295
+ }
1296
+ // Sort by Y position (top to bottom), then X (left to right)
1297
+ texts.sort((a, b) => a.centerY - b.centerY || a.centerX - b.centerX);
1298
+ const textList = texts.map((t) => `"${t.text}" at (${t.centerX}, ${t.centerY})`).join("\n");
1299
+ return {
1300
+ content: [
1301
+ {
1302
+ type: "text",
1303
+ text: `Found ${texts.length} text elements:\n${textList}`,
1304
+ },
1305
+ ],
1306
+ };
1307
+ });
1308
+ // =====================================================
1309
+ // TOOL: is_keyboard_visible
1310
+ // =====================================================
1311
+ server.tool("is_keyboard_visible", "Check if the soft keyboard is currently visible on screen", {}, async () => {
1312
+ let isShowingViaIme = false;
1313
+ let hasKeyboardWindow = false;
1314
+ let heightMethod = false;
1315
+ // Method 1: Check InputMethod visibility via dumpsys
1316
+ try {
1317
+ const imeDump = await shell("dumpsys input_method | grep mInputShown || true");
1318
+ isShowingViaIme = imeDump.includes("mInputShown=true");
1319
+ }
1320
+ catch {
1321
+ // Ignore errors
1322
+ }
1323
+ // Method 2: Check if keyboard window is visible
1324
+ try {
1325
+ const windowDump = await shell("dumpsys window windows | grep -i inputmethod || true");
1326
+ hasKeyboardWindow = windowDump.toLowerCase().includes("inputmethod") &&
1327
+ windowDump.includes("mHasSurface=true");
1328
+ }
1329
+ catch {
1330
+ // Ignore errors
1331
+ }
1332
+ // Method 3: Check visible height vs screen height
1333
+ try {
1334
+ const visibleFrame = await shell("dumpsys window | grep 'mVisibleFrame' || true");
1335
+ const sizeOutput = await shell("wm size");
1336
+ const sizeMatch = sizeOutput.match(/(\d+)x(\d+)/);
1337
+ if (sizeMatch && visibleFrame) {
1338
+ const screenHeight = parseInt(sizeMatch[2]);
1339
+ const frameMatch = visibleFrame.match(/mVisibleFrame=\[\d+,\d+\]\[\d+,(\d+)\]/);
1340
+ if (frameMatch) {
1341
+ const visibleHeight = parseInt(frameMatch[1]);
1342
+ // If visible area is significantly less than screen, keyboard is likely shown
1343
+ heightMethod = visibleHeight < screenHeight * 0.8;
1344
+ }
1345
+ }
1346
+ }
1347
+ catch {
1348
+ // Ignore height method errors
1349
+ }
1350
+ const isVisible = isShowingViaIme || hasKeyboardWindow || heightMethod;
1351
+ return {
1352
+ content: [
1353
+ {
1354
+ type: "text",
1355
+ text: JSON.stringify({
1356
+ visible: isVisible,
1357
+ checks: {
1358
+ inputMethodShown: isShowingViaIme,
1359
+ keyboardWindowVisible: hasKeyboardWindow,
1360
+ heightReduced: heightMethod,
1361
+ },
1362
+ }, null, 2),
1363
+ },
1364
+ ],
1365
+ };
1366
+ });
1367
+ // =====================================================
1368
+ // TOOL: get_focused_input_value
1369
+ // =====================================================
1370
+ server.tool("get_focused_input_value", "Get the current text value of the focused input field", {}, async () => {
1371
+ await shell("uiautomator dump /sdcard/ui_dump.xml");
1372
+ const xml = await shell("cat /sdcard/ui_dump.xml");
1373
+ // Look for focused element that is an input field (EditText or similar)
1374
+ // Pattern matches focused="true" along with text attribute
1375
+ const patterns = [
1376
+ // Pattern 1: focused before text
1377
+ /class="[^"]*(?:Edit|Input|Text)[^"]*"[^>]*focused="true"[^>]*text="([^"]*)"/gi,
1378
+ // Pattern 2: text before focused
1379
+ /class="[^"]*(?:Edit|Input|Text)[^"]*"[^>]*text="([^"]*)"[^>]*focused="true"/gi,
1380
+ // Pattern 3: Generic focused with text
1381
+ /focused="true"[^>]*text="([^"]*)"[^>]*class="[^"]*(?:Edit|Input|Text)[^"]*"/gi,
1382
+ ];
1383
+ for (const pattern of patterns) {
1384
+ const match = pattern.exec(xml);
1385
+ if (match) {
1386
+ return {
1387
+ content: [
1388
+ {
1389
+ type: "text",
1390
+ text: JSON.stringify({
1391
+ found: true,
1392
+ value: match[1],
1393
+ isEmpty: match[1] === "",
1394
+ }, null, 2),
1395
+ },
1396
+ ],
1397
+ };
1398
+ }
1399
+ }
1400
+ // Try broader search for any focused element with text
1401
+ const broadPattern = /focused="true"[^>]*text="([^"]*)"|text="([^"]*)"[^>]*focused="true"/gi;
1402
+ const broadMatch = broadPattern.exec(xml);
1403
+ if (broadMatch) {
1404
+ const value = broadMatch[1] || broadMatch[2] || "";
1405
+ return {
1406
+ content: [
1407
+ {
1408
+ type: "text",
1409
+ text: JSON.stringify({
1410
+ found: true,
1411
+ value,
1412
+ isEmpty: value === "",
1413
+ note: "Found focused element (may not be an input field)",
1414
+ }, null, 2),
1415
+ },
1416
+ ],
1417
+ };
1418
+ }
1419
+ return {
1420
+ content: [
1421
+ {
1422
+ type: "text",
1423
+ text: JSON.stringify({
1424
+ found: false,
1425
+ value: null,
1426
+ error: "No focused input field found",
1427
+ }, null, 2),
1428
+ },
1429
+ ],
1430
+ };
1431
+ });
1220
1432
  // Start server
1221
1433
  async function main() {
1222
1434
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-android-emulator",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "MCP Server for Android Emulator interaction via ADB - enables AI assistants to control Android devices",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -47,7 +47,7 @@ async function shell(command: string): Promise<string> {
47
47
  // Create MCP server
48
48
  const server = new McpServer({
49
49
  name: "android-emulator",
50
- version: "1.2.3",
50
+ version: "1.3.0",
51
51
  });
52
52
 
53
53
  // =====================================================
@@ -530,13 +530,48 @@ server.tool(
530
530
  "Get the currently focused activity/screen",
531
531
  {},
532
532
  async () => {
533
- const result = await shell("dumpsys activity activities | grep mResumedActivity");
533
+ let activity = "Unknown";
534
+
535
+ // Try multiple methods for compatibility across emulators
536
+ try {
537
+ // Method 1: mResumedActivity (standard Android)
538
+ const result1 = await shell("dumpsys activity activities | grep -E 'mResumedActivity|mCurrentFocus' || true");
539
+ if (result1 && result1.trim()) {
540
+ activity = result1.trim();
541
+ }
542
+ } catch {
543
+ // Ignore
544
+ }
545
+
546
+ if (activity === "Unknown") {
547
+ try {
548
+ // Method 2: topActivity (alternative)
549
+ const result2 = await shell("dumpsys activity top | head -5 || true");
550
+ if (result2 && result2.trim()) {
551
+ activity = result2.trim();
552
+ }
553
+ } catch {
554
+ // Ignore
555
+ }
556
+ }
557
+
558
+ if (activity === "Unknown") {
559
+ try {
560
+ // Method 3: window focus (Redroid/Docker compatible)
561
+ const result3 = await shell("dumpsys window | grep -E 'mCurrentFocus|mFocusedApp' || true");
562
+ if (result3 && result3.trim()) {
563
+ activity = result3.trim();
564
+ }
565
+ } catch {
566
+ // Ignore
567
+ }
568
+ }
534
569
 
535
570
  return {
536
571
  content: [
537
572
  {
538
573
  type: "text",
539
- text: `Current activity: ${result.trim()}`,
574
+ text: `Current activity:\n${activity}`,
540
575
  },
541
576
  ],
542
577
  };
@@ -1016,6 +1051,27 @@ server.tool(
1016
1051
  // =====================================================
1017
1052
  // TOOL: wait_for_ui_stable
1018
1053
  // =====================================================
1054
+ /**
1055
+ * Extract a normalized fingerprint of UI elements from XML
1056
+ * Only considers text, bounds, and class - ignores dynamic attributes
1057
+ */
1058
+ function extractUIFingerprint(xml: string): string {
1059
+ const elements: string[] = [];
1060
+ // Match elements with text or class and bounds
1061
+ const regex = /(?:text="([^"]*)")?[^>]*(?:class="([^"]*)")?[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g;
1062
+ let match;
1063
+
1064
+ while ((match = regex.exec(xml)) !== null) {
1065
+ const [, text, className, x1, y1, x2, y2] = match;
1066
+ // Only include elements with text or meaningful classes
1067
+ if (text || className) {
1068
+ elements.push(`${text || ""}|${className || ""}|${x1},${y1},${x2},${y2}`);
1069
+ }
1070
+ }
1071
+
1072
+ return elements.sort().join("\n");
1073
+ }
1074
+
1019
1075
  server.tool(
1020
1076
  "wait_for_ui_stable",
1021
1077
  "Wait for the UI to stop changing (useful after animations)",
@@ -1025,28 +1081,30 @@ server.tool(
1025
1081
  },
1026
1082
  async ({ timeout = 5000, checkInterval = 500 }) => {
1027
1083
  const startTime = Date.now();
1028
- let lastXml = "";
1084
+ let lastFingerprint = "";
1029
1085
  let stableCount = 0;
1030
1086
 
1031
1087
  while (Date.now() - startTime < timeout) {
1032
1088
  await shell("uiautomator dump /sdcard/ui_dump.xml");
1033
1089
  const currentXml = await shell("cat /sdcard/ui_dump.xml");
1090
+ const currentFingerprint = extractUIFingerprint(currentXml);
1034
1091
 
1035
- if (currentXml === lastXml) {
1092
+ if (currentFingerprint === lastFingerprint) {
1036
1093
  stableCount++;
1037
1094
  if (stableCount >= 2) {
1095
+ const elapsed = Date.now() - startTime;
1038
1096
  return {
1039
1097
  content: [
1040
1098
  {
1041
1099
  type: "text",
1042
- text: `UI stable after ${Math.round((Date.now() - startTime) / 1000)}s`,
1100
+ text: `UI stable after ${elapsed < 1000 ? elapsed + "ms" : Math.round(elapsed / 1000) + "s"}`,
1043
1101
  },
1044
1102
  ],
1045
1103
  };
1046
1104
  }
1047
1105
  } else {
1048
1106
  stableCount = 0;
1049
- lastXml = currentXml;
1107
+ lastFingerprint = currentFingerprint;
1050
1108
  }
1051
1109
 
1052
1110
  await new Promise((resolve) => setTimeout(resolve, checkInterval));
@@ -1559,6 +1617,193 @@ server.tool(
1559
1617
  }
1560
1618
  );
1561
1619
 
1620
+ // =====================================================
1621
+ // TOOL: get_all_text
1622
+ // =====================================================
1623
+ server.tool(
1624
+ "get_all_text",
1625
+ "Get all visible text elements on screen (useful for debugging and verification)",
1626
+ {
1627
+ includeEmpty: z.boolean().optional().describe("Include elements with empty text (default: false)"),
1628
+ },
1629
+ async ({ includeEmpty = false }) => {
1630
+ await shell("uiautomator dump /sdcard/ui_dump.xml");
1631
+ const xml = await shell("cat /sdcard/ui_dump.xml");
1632
+
1633
+ const texts: Array<{ text: string; centerX: number; centerY: number }> = [];
1634
+ const regex = /text="([^"]*)"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/g;
1635
+ let match;
1636
+
1637
+ while ((match = regex.exec(xml)) !== null) {
1638
+ const [, text, x1, y1, x2, y2] = match;
1639
+ if (text || includeEmpty) {
1640
+ texts.push({
1641
+ text: text || "(empty)",
1642
+ centerX: Math.round((parseInt(x1) + parseInt(x2)) / 2),
1643
+ centerY: Math.round((parseInt(y1) + parseInt(y2)) / 2),
1644
+ });
1645
+ }
1646
+ }
1647
+
1648
+ // Sort by Y position (top to bottom), then X (left to right)
1649
+ texts.sort((a, b) => a.centerY - b.centerY || a.centerX - b.centerX);
1650
+
1651
+ const textList = texts.map((t) => `"${t.text}" at (${t.centerX}, ${t.centerY})`).join("\n");
1652
+
1653
+ return {
1654
+ content: [
1655
+ {
1656
+ type: "text",
1657
+ text: `Found ${texts.length} text elements:\n${textList}`,
1658
+ },
1659
+ ],
1660
+ };
1661
+ }
1662
+ );
1663
+
1664
+ // =====================================================
1665
+ // TOOL: is_keyboard_visible
1666
+ // =====================================================
1667
+ server.tool(
1668
+ "is_keyboard_visible",
1669
+ "Check if the soft keyboard is currently visible on screen",
1670
+ {},
1671
+ async () => {
1672
+ let isShowingViaIme = false;
1673
+ let hasKeyboardWindow = false;
1674
+ let heightMethod = false;
1675
+
1676
+ // Method 1: Check InputMethod visibility via dumpsys
1677
+ try {
1678
+ const imeDump = await shell("dumpsys input_method | grep mInputShown || true");
1679
+ isShowingViaIme = imeDump.includes("mInputShown=true");
1680
+ } catch {
1681
+ // Ignore errors
1682
+ }
1683
+
1684
+ // Method 2: Check if keyboard window is visible
1685
+ try {
1686
+ const windowDump = await shell("dumpsys window windows | grep -i inputmethod || true");
1687
+ hasKeyboardWindow = windowDump.toLowerCase().includes("inputmethod") &&
1688
+ windowDump.includes("mHasSurface=true");
1689
+ } catch {
1690
+ // Ignore errors
1691
+ }
1692
+
1693
+ // Method 3: Check visible height vs screen height
1694
+ try {
1695
+ const visibleFrame = await shell("dumpsys window | grep 'mVisibleFrame' || true");
1696
+ const sizeOutput = await shell("wm size");
1697
+ const sizeMatch = sizeOutput.match(/(\d+)x(\d+)/);
1698
+ if (sizeMatch && visibleFrame) {
1699
+ const screenHeight = parseInt(sizeMatch[2]);
1700
+ const frameMatch = visibleFrame.match(/mVisibleFrame=\[\d+,\d+\]\[\d+,(\d+)\]/);
1701
+ if (frameMatch) {
1702
+ const visibleHeight = parseInt(frameMatch[1]);
1703
+ // If visible area is significantly less than screen, keyboard is likely shown
1704
+ heightMethod = visibleHeight < screenHeight * 0.8;
1705
+ }
1706
+ }
1707
+ } catch {
1708
+ // Ignore height method errors
1709
+ }
1710
+
1711
+ const isVisible = isShowingViaIme || hasKeyboardWindow || heightMethod;
1712
+
1713
+ return {
1714
+ content: [
1715
+ {
1716
+ type: "text",
1717
+ text: JSON.stringify({
1718
+ visible: isVisible,
1719
+ checks: {
1720
+ inputMethodShown: isShowingViaIme,
1721
+ keyboardWindowVisible: hasKeyboardWindow,
1722
+ heightReduced: heightMethod,
1723
+ },
1724
+ }, null, 2),
1725
+ },
1726
+ ],
1727
+ };
1728
+ }
1729
+ );
1730
+
1731
+ // =====================================================
1732
+ // TOOL: get_focused_input_value
1733
+ // =====================================================
1734
+ server.tool(
1735
+ "get_focused_input_value",
1736
+ "Get the current text value of the focused input field",
1737
+ {},
1738
+ async () => {
1739
+ await shell("uiautomator dump /sdcard/ui_dump.xml");
1740
+ const xml = await shell("cat /sdcard/ui_dump.xml");
1741
+
1742
+ // Look for focused element that is an input field (EditText or similar)
1743
+ // Pattern matches focused="true" along with text attribute
1744
+ const patterns = [
1745
+ // Pattern 1: focused before text
1746
+ /class="[^"]*(?:Edit|Input|Text)[^"]*"[^>]*focused="true"[^>]*text="([^"]*)"/gi,
1747
+ // Pattern 2: text before focused
1748
+ /class="[^"]*(?:Edit|Input|Text)[^"]*"[^>]*text="([^"]*)"[^>]*focused="true"/gi,
1749
+ // Pattern 3: Generic focused with text
1750
+ /focused="true"[^>]*text="([^"]*)"[^>]*class="[^"]*(?:Edit|Input|Text)[^"]*"/gi,
1751
+ ];
1752
+
1753
+ for (const pattern of patterns) {
1754
+ const match = pattern.exec(xml);
1755
+ if (match) {
1756
+ return {
1757
+ content: [
1758
+ {
1759
+ type: "text",
1760
+ text: JSON.stringify({
1761
+ found: true,
1762
+ value: match[1],
1763
+ isEmpty: match[1] === "",
1764
+ }, null, 2),
1765
+ },
1766
+ ],
1767
+ };
1768
+ }
1769
+ }
1770
+
1771
+ // Try broader search for any focused element with text
1772
+ const broadPattern = /focused="true"[^>]*text="([^"]*)"|text="([^"]*)"[^>]*focused="true"/gi;
1773
+ const broadMatch = broadPattern.exec(xml);
1774
+
1775
+ if (broadMatch) {
1776
+ const value = broadMatch[1] || broadMatch[2] || "";
1777
+ return {
1778
+ content: [
1779
+ {
1780
+ type: "text",
1781
+ text: JSON.stringify({
1782
+ found: true,
1783
+ value,
1784
+ isEmpty: value === "",
1785
+ note: "Found focused element (may not be an input field)",
1786
+ }, null, 2),
1787
+ },
1788
+ ],
1789
+ };
1790
+ }
1791
+
1792
+ return {
1793
+ content: [
1794
+ {
1795
+ type: "text",
1796
+ text: JSON.stringify({
1797
+ found: false,
1798
+ value: null,
1799
+ error: "No focused input field found",
1800
+ }, null, 2),
1801
+ },
1802
+ ],
1803
+ };
1804
+ }
1805
+ );
1806
+
1562
1807
  // Start server
1563
1808
  async function main() {
1564
1809
  const transport = new StdioServerTransport();