android-mcp-toolkit 1.2.0 → 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 (2) hide show
  1. package/dist/index.js +433 -337
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -4,11 +4,160 @@ var __commonJS = (cb, mod) => function __require() {
4
4
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
5
5
  };
6
6
 
7
+ // vendor/svg2vectordrawable/svgo-adapter.js
8
+ var require_svgo_adapter = __commonJS({
9
+ "vendor/svg2vectordrawable/svgo-adapter.js"(exports2, module2) {
10
+ var { optimize } = require("svgo");
11
+ var JSAPI = class _JSAPI {
12
+ constructor(data, parentNode) {
13
+ this.parentNode = parentNode || null;
14
+ this.type = data.type || "element";
15
+ this.name = data.name || "";
16
+ this.children = [];
17
+ this.attrs = {};
18
+ if (data.attributes) {
19
+ for (const [key, value] of Object.entries(data.attributes)) {
20
+ this._addAttrInternal(key, value);
21
+ }
22
+ } else if (data.attrs) {
23
+ this.attrs = data.attrs;
24
+ }
25
+ if (data.children && Array.isArray(data.children)) {
26
+ this.children = data.children.map((c) => new _JSAPI(c, this));
27
+ }
28
+ }
29
+ _addAttrInternal(name, value) {
30
+ const parts = name.split(":");
31
+ let local = parts[0];
32
+ let prefix = "";
33
+ if (parts.length > 1) {
34
+ prefix = parts[0];
35
+ local = parts[1];
36
+ }
37
+ this.attrs[name] = {
38
+ name,
39
+ value,
40
+ local,
41
+ prefix
42
+ };
43
+ }
44
+ // Legacy JSAPI text node handling?
45
+ // SVGO v2 had text nodes? XAST has { type: 'text', value: '...' }
46
+ // Android VectorDrawable doesn't support text, so maybe it's ignored or handled?
47
+ // The converter doesn't seem to handle text nodes explicitly, it iterates children.
48
+ hasAttr(name, value) {
49
+ const attr = this.attrs[name];
50
+ if (!attr) return false;
51
+ if (value !== void 0) return attr.value === value;
52
+ return true;
53
+ }
54
+ attr(name) {
55
+ return this.attrs[name];
56
+ }
57
+ addAttr(attrObj) {
58
+ this.attrs[attrObj.name] = attrObj;
59
+ }
60
+ removeAttr(name) {
61
+ delete this.attrs[name];
62
+ }
63
+ renameElem(newName) {
64
+ this.name = newName;
65
+ }
66
+ eachAttr(callback, context) {
67
+ for (const key in this.attrs) {
68
+ callback.call(context || this, this.attrs[key]);
69
+ }
70
+ }
71
+ isEmpty() {
72
+ return !this.children || this.children.length === 0;
73
+ }
74
+ // Helper to find specific children (used in converter?)
75
+ // converter uses querySelectorAll on `data` (which is Root node)
76
+ // We need to implement querySelectorAll if it was part of JSAPI or SVGO API.
77
+ // Looking at converter:
78
+ // `data.querySelectorAll('use')`
79
+ // `root.querySelector('svg')`
80
+ // Wait, JSAPI v2 had querySelector/All?
81
+ // If so, I MUST implement them.
82
+ querySelector(selector) {
83
+ const results = this.querySelectorAll(selector);
84
+ return results.length > 0 ? results[0] : null;
85
+ }
86
+ querySelectorAll(selector) {
87
+ const results = [];
88
+ this._traverse((node) => {
89
+ if (this._matches(node, selector)) {
90
+ results.push(node);
91
+ }
92
+ });
93
+ return results;
94
+ }
95
+ _traverse(callback) {
96
+ callback(this);
97
+ if (this.children) {
98
+ this.children.forEach((c) => c._traverse(callback));
99
+ }
100
+ }
101
+ _matches(node, selector) {
102
+ if (node.type !== "element") return false;
103
+ if (selector.includes(",")) {
104
+ const parts = selector.split(",").map((s) => s.trim());
105
+ return parts.some((p) => this._matches(node, p));
106
+ }
107
+ if (/^[a-zA-Z0-9\-_:]+$/.test(selector)) {
108
+ return node.name === selector;
109
+ }
110
+ const attrMatch = selector.match(/^([a-zA-Z0-9\-_:]+)?\[([a-zA-Z0-9\-_:]+)="([^"]+)"\]$/);
111
+ if (attrMatch) {
112
+ const tagName = attrMatch[1];
113
+ const attrName = attrMatch[2];
114
+ const attrVal = attrMatch[3];
115
+ if (tagName && node.name !== tagName) return false;
116
+ return node.hasAttr(attrName, attrVal);
117
+ }
118
+ return false;
119
+ }
120
+ spliceContent(index, count, newItems) {
121
+ const items = Array.isArray(newItems) ? newItems : [newItems];
122
+ const validItems = items.filter((i) => i && (i instanceof _JSAPI || Array.isArray(i) && i.length === 0 ? false : true));
123
+ const flatItems = items.flat();
124
+ flatItems.forEach((item) => {
125
+ if (item instanceof _JSAPI) {
126
+ item.parentNode = this;
127
+ }
128
+ });
129
+ this.children.splice(index, count, ...flatItems);
130
+ }
131
+ };
132
+ function parseSvg(svgString) {
133
+ let xastRoot = null;
134
+ optimize(svgString, {
135
+ plugins: [
136
+ {
137
+ name: "fetch-ast",
138
+ fn: (root) => {
139
+ xastRoot = root;
140
+ return {};
141
+ }
142
+ }
143
+ ]
144
+ });
145
+ if (!xastRoot) {
146
+ throw new Error("SVGO failed to parse SVG");
147
+ }
148
+ return new JSAPI(xastRoot);
149
+ }
150
+ module2.exports = {
151
+ JSAPI,
152
+ parseSvg
153
+ };
154
+ }
155
+ });
156
+
7
157
  // vendor/svg2vectordrawable/svg-to-vectordrawable.js
8
158
  var require_svg_to_vectordrawable = __commonJS({
9
159
  "vendor/svg2vectordrawable/svg-to-vectordrawable.js"(exports2, module2) {
10
- var { parseSvg } = require("svgo/lib/parser");
11
- var JSAPI = require("svgo/lib/svgo/jsAPI");
160
+ var { parseSvg, JSAPI } = require_svgo_adapter();
12
161
  var pathBounds = require("svg-path-bounds");
13
162
  var svgpath = require("svgpath");
14
163
  var JS2XML = function() {
@@ -908,189 +1057,50 @@ var require_svg_to_vectordrawable = __commonJS({
908
1057
  var require_svgo_config = __commonJS({
909
1058
  "vendor/svg2vectordrawable/svgo-config.js"(exports2, module2) {
910
1059
  module2.exports = function(floatPrecision = 2) {
911
- const svgoConfig = {
912
- info: {
913
- input: "string"
914
- },
1060
+ return {
915
1061
  plugins: [
916
1062
  {
917
- name: "removeDoctype"
918
- },
919
- {
920
- name: "removeXMLProcInst"
921
- },
922
- {
923
- name: "removeComments"
924
- },
925
- {
926
- name: "removeMetadata"
927
- },
928
- {
929
- name: "removeEditorsNSData"
930
- },
931
- {
932
- name: "cleanupAttrs"
933
- },
934
- {
935
- name: "mergeStyles"
936
- },
937
- {
938
- name: "inlineStyles",
939
- params: { onlyMatchedOnce: false }
940
- },
941
- {
942
- name: "minifyStyles"
943
- },
944
- {
945
- name: "cleanupIDs",
946
- active: false
947
- },
948
- {
949
- name: "removeUselessDefs"
950
- },
951
- {
952
- name: "cleanupNumericValues",
953
- params: { floatPrecision, leadingZero: false }
954
- },
955
- {
956
- name: "convertColors",
957
- params: { shorthex: false, shortname: false }
958
- },
959
- {
960
- name: "removeUnknownsAndDefaults",
961
- params: { unknownContent: false, unknownAttrs: false }
962
- },
963
- {
964
- name: "removeNonInheritableGroupAttrs"
965
- },
966
- {
967
- name: "removeUselessStrokeAndFill"
968
- },
969
- {
970
- name: "removeViewBox",
971
- active: false
972
- },
973
- {
974
- name: "cleanupEnableBackground"
975
- },
976
- {
977
- name: "removeHiddenElems"
978
- },
979
- {
980
- name: "removeEmptyText"
981
- },
982
- {
983
- name: "convertShapeToPath",
984
- params: { convertArcs: true, floatPrecision }
985
- },
986
- {
987
- name: "convertEllipseToCircle"
988
- },
989
- {
990
- name: "moveElemsAttrsToGroup",
991
- active: false
992
- },
993
- {
994
- name: "moveGroupAttrsToElems"
995
- },
996
- {
997
- name: "collapseGroups"
998
- },
999
- {
1000
- name: "convertPathData",
1001
- params: { floatPrecision, transformPrecision: floatPrecision, leadingZero: false, makeArcs: false, noSpaceAfterFlags: false, collapseRepeated: false }
1002
- },
1003
- {
1004
- name: "convertTransform"
1005
- },
1006
- {
1007
- name: "removeEmptyAttrs"
1008
- },
1009
- {
1010
- name: "removeEmptyContainers"
1011
- },
1012
- {
1013
- name: "mergePaths",
1014
- active: false
1015
- },
1016
- {
1017
- name: "removeUnusedNS"
1018
- },
1019
- {
1020
- name: "sortDefsChildren"
1021
- },
1022
- {
1023
- name: "removeTitle"
1024
- },
1025
- {
1026
- name: "removeDesc"
1027
- },
1028
- {
1029
- name: "removeXMLNS",
1030
- active: false
1063
+ name: "preset-default",
1064
+ params: {
1065
+ overrides: {
1066
+ // Disable things that were active: false
1067
+ cleanupIds: false,
1068
+ mergePaths: false,
1069
+ // active: false in legacy
1070
+ // Parameter overrides
1071
+ convertPathData: {
1072
+ floatPrecision,
1073
+ transformPrecision: floatPrecision,
1074
+ leadingZero: false,
1075
+ makeArcs: false,
1076
+ noSpaceAfterFlags: false,
1077
+ collapseRepeated: false
1078
+ },
1079
+ cleanupNumericValues: {
1080
+ floatPrecision,
1081
+ leadingZero: false
1082
+ },
1083
+ convertShapeToPath: {
1084
+ convertArcs: true,
1085
+ floatPrecision
1086
+ }
1087
+ // convertColors: { shorthex: false, shortname: false } // Legacy params
1088
+ }
1089
+ }
1031
1090
  },
1091
+ // Additional plugins explicitly enabled in legacy
1032
1092
  {
1033
1093
  name: "removeRasterImages"
1034
1094
  },
1035
1095
  {
1036
- name: "cleanupListOfValues",
1037
- params: { floatPrecision, leadingZero: false }
1038
- },
1039
- {
1040
- name: "sortAttrs",
1041
- active: false
1042
- },
1043
- {
1044
- name: "convertStyleToAttrs",
1045
- active: false
1046
- },
1047
- {
1048
- name: "prefixIds",
1049
- active: false
1050
- },
1051
- {
1052
- name: "removeDimensions",
1053
- active: false
1054
- },
1055
- {
1056
- name: "removeAttrs",
1057
- active: false
1058
- },
1059
- {
1060
- name: "removeAttributesBySelector",
1061
- active: false
1062
- },
1063
- {
1064
- name: "removeElementsByAttr",
1065
- active: false
1066
- },
1067
- {
1068
- name: "addClassesToSVGElement",
1069
- active: false
1070
- },
1071
- {
1072
- name: "removeStyleElement",
1073
- active: false
1074
- },
1075
- {
1076
- name: "removeScriptElement",
1077
- active: false
1078
- },
1079
- {
1080
- name: "addAttributesToSVGElement",
1081
- active: false
1082
- },
1083
- {
1084
- name: "removeOffCanvasPaths",
1085
- active: false
1086
- },
1087
- {
1088
- name: "reusePaths",
1089
- active: false
1096
+ name: "convertColors",
1097
+ params: {
1098
+ shorthex: false,
1099
+ shortname: false
1100
+ }
1090
1101
  }
1091
1102
  ]
1092
1103
  };
1093
- return svgoConfig;
1094
1104
  };
1095
1105
  }
1096
1106
  });
@@ -1245,42 +1255,21 @@ var require_logcatTool = __commonJS({
1245
1255
  var z = require("zod/v4");
1246
1256
  var execFileAsync = promisify(execFile);
1247
1257
  var logcatToolInstructions2 = [
1248
- "Use read-adb-logcat to tail device logs for a package, pid, or tag; default tail=200 lines.",
1249
- "Use get-pid-by-package to resolve pid quickly via adb shell pidof -s.",
1250
- "Use get-current-activity to inspect current focus (Activity/Window) via dumpsys window.",
1251
- "Use fetch-crash-stacktrace to pull the latest crash buffer (-b crash) optionally filtered by pid.",
1252
- "Use check-anr-state to inspect ActivityManager ANR logs and /data/anr/traces.txt (best-effort).",
1253
- "Use clear-logcat-buffer to reset logcat (-c) before running new scenarios."
1258
+ "Use manage-logcat to read logs, fetch crash stacktraces, check ANR state, or clear logcat buffers.",
1259
+ "Use get-current-activity to inspect current focus (Activity/Window) via dumpsys window."
1254
1260
  ].join("\n");
1255
- var logcatInputSchema = z.object({
1261
+ var manageLogcatSchema = z.object({
1262
+ action: z.enum(["read", "crash", "anr", "clear"]).default("read").describe("Action to perform: read logs, get crash buffer, check ANR, or clear buffer."),
1256
1263
  packageName: z.string().min(1).describe("Android package name; resolves pid via adb shell pidof").optional(),
1257
1264
  pid: z.string().min(1).describe("Explicit process id for logcat --pid").optional(),
1258
1265
  tag: z.string().min(1).describe("Logcat tag to include (uses -s tag)").optional(),
1259
- priority: z.enum(["V", "D", "I", "W", "E", "F", "S"]).default("V").describe("Minimum priority when tag is provided (e.g., D for debug)"),
1260
- maxLines: z.number().int().min(1).max(2e3).default(200).describe("Tail line count via logcat -t"),
1261
- timeoutMs: z.number().int().min(1e3).max(15e3).default(5e3).describe("Timeout per adb call in milliseconds")
1262
- }).refine((data) => data.packageName || data.pid || data.tag, {
1263
- message: "Provide packageName, pid, or tag to avoid unfiltered logs"
1264
- });
1265
- var pidInputSchema = z.object({
1266
- packageName: z.string().min(1).describe("Android package name to resolve pid via adb shell pidof -s"),
1266
+ priority: z.enum(["V", "D", "I", "W", "E", "F", "S"]).default("V").describe("Minimum priority (e.g. D for debug)."),
1267
+ maxLines: z.number().int().min(1).max(2e3).default(200).describe("Tail line count (logcat -t)."),
1267
1268
  timeoutMs: z.number().int().min(1e3).max(15e3).default(5e3).describe("Timeout per adb call in milliseconds")
1268
1269
  });
1269
1270
  var currentActivityInputSchema = z.object({
1270
1271
  timeoutMs: z.number().int().min(1e3).max(15e3).default(5e3).describe("Timeout per adb call in milliseconds")
1271
1272
  });
1272
- var crashStackInputSchema = z.object({
1273
- packageName: z.string().min(1).describe("Optional package to resolve pid; filters crash buffer with --pid").optional(),
1274
- maxLines: z.number().int().min(50).max(2e3).default(400).describe("Tail line count from crash buffer (-b crash -t)"),
1275
- timeoutMs: z.number().int().min(1e3).max(15e3).default(5e3).describe("Timeout per adb call in milliseconds")
1276
- });
1277
- var anrStateInputSchema = z.object({
1278
- maxLines: z.number().int().min(50).max(2e3).default(400).describe("Tail line count from ActivityManager:E"),
1279
- timeoutMs: z.number().int().min(1e3).max(15e3).default(5e3).describe("Timeout per adb call in milliseconds")
1280
- });
1281
- var clearLogcatInputSchema = z.object({
1282
- timeoutMs: z.number().int().min(1e3).max(15e3).default(5e3).describe("Timeout per adb call in milliseconds")
1283
- });
1284
1273
  async function runAdbCommand(args, timeoutMs) {
1285
1274
  try {
1286
1275
  const { stdout } = await execFileAsync("adb", args, {
@@ -1298,158 +1287,76 @@ var require_logcatTool = __commonJS({
1298
1287
  }
1299
1288
  }
1300
1289
  async function resolvePid(packageName, timeoutMs) {
1301
- const output = await runAdbCommand(["shell", "pidof", "-s", packageName], timeoutMs);
1302
- const pid = output.split(/\s+/).find(Boolean);
1303
- if (!pid) {
1304
- throw new Error(`Could not resolve pid for package ${packageName}`);
1305
- }
1306
- return pid;
1307
- }
1308
- function buildLogcatArgs(params, pid) {
1309
- const args = ["logcat", "-d", "-t", String(params.maxLines)];
1310
- if (pid) {
1311
- args.push(`--pid=${pid}`);
1312
- }
1313
- if (params.tag) {
1314
- const filterSpec = `${params.tag}:${params.priority}`;
1315
- args.push("-s", filterSpec);
1290
+ try {
1291
+ const output = await runAdbCommand(["shell", "pidof", "-s", packageName], timeoutMs);
1292
+ const pid = output.split(/\s+/).find(Boolean);
1293
+ return pid || null;
1294
+ } catch (e) {
1295
+ return null;
1316
1296
  }
1317
- return args;
1318
1297
  }
1319
1298
  function registerLogcatTool2(server2) {
1320
1299
  server2.registerTool(
1321
- "read-adb-logcat",
1300
+ "manage-logcat",
1322
1301
  {
1323
- title: "Read adb logcat",
1324
- description: "Dump recent adb logcat output scoped by package, pid, or tag with tail and timeout controls.",
1325
- inputSchema: logcatInputSchema
1302
+ title: "Manage ADB Logcat",
1303
+ description: "Unified tool to read logs, capture crashes, check ANRs, and clear buffers.",
1304
+ inputSchema: manageLogcatSchema
1326
1305
  },
1327
- async (params, extra) => {
1328
- const timeoutMs = params.timeoutMs;
1329
- const pid = params.pid || (params.packageName ? await resolvePid(params.packageName, timeoutMs) : null);
1330
- const args = buildLogcatArgs(params, pid);
1331
- const startTime = process.hrtime.bigint();
1332
- const output = await runAdbCommand(args, timeoutMs);
1333
- const elapsedMs = Number(process.hrtime.bigint() - startTime) / 1e6;
1334
- if (extra && typeof extra.sessionId === "string") {
1335
- server2.sendLoggingMessage(
1336
- {
1337
- level: "info",
1338
- data: `Read logcat (${params.maxLines} lines` + (pid ? `, pid=${pid}` : "") + (params.tag ? `, tag=${params.tag}:${params.priority}` : "") + `) in ${elapsedMs.toFixed(2)}ms`
1339
- },
1340
- extra.sessionId
1341
- ).catch(() => {
1342
- });
1306
+ async (params) => {
1307
+ const { action, timeoutMs } = params;
1308
+ if (action === "clear") {
1309
+ await runAdbCommand(["logcat", "-c"], timeoutMs);
1310
+ return { content: [{ type: "text", text: "Cleared logcat buffers." }] };
1343
1311
  }
1344
- if (!output) {
1345
- return { content: [{ type: "text", text: "Logcat returned no lines." }] };
1312
+ let pid = params.pid;
1313
+ if (!pid && params.packageName) {
1314
+ pid = await resolvePid(params.packageName, timeoutMs);
1346
1315
  }
1347
- return { content: [{ type: "text", text: output }] };
1348
- }
1349
- );
1350
- server2.registerTool(
1351
- "get-pid-by-package",
1352
- {
1353
- title: "Get pid by package",
1354
- description: "Resolve process id for a package via adb shell pidof -s.",
1355
- inputSchema: pidInputSchema
1356
- },
1357
- async (params) => {
1358
- const pid = await resolvePid(params.packageName, params.timeoutMs);
1359
- return { content: [{ type: "text", text: pid }] };
1316
+ if (action === "anr") {
1317
+ const sections = [];
1318
+ try {
1319
+ const logArgs = ["logcat", "-d", "-t", String(params.maxLines), "ActivityManager:E", "*:S"];
1320
+ const amLogs = await runAdbCommand(logArgs, timeoutMs);
1321
+ sections.push("ActivityManager (recent):\n" + (amLogs || "No entries."));
1322
+ } catch (e) {
1323
+ sections.push("ActivityManager error: " + e.message);
1324
+ }
1325
+ try {
1326
+ const tail = await runAdbCommand(["shell", "tail", "-n", "200", "/data/anr/traces.txt"], timeoutMs);
1327
+ sections.push("traces.txt tail (200 lines):\n" + (tail || "Empty."));
1328
+ } catch (e) {
1329
+ sections.push("traces.txt error: " + e.message);
1330
+ }
1331
+ return { content: [{ type: "text", text: sections.join("\n\n") }] };
1332
+ }
1333
+ if (action === "crash") {
1334
+ const args2 = ["logcat", "-b", "crash", "-d", "-t", String(params.maxLines)];
1335
+ if (pid) args2.push(`--pid=${pid}`);
1336
+ const output2 = await runAdbCommand(args2, timeoutMs);
1337
+ return { content: [{ type: "text", text: output2 || "No crash entries found." }] };
1338
+ }
1339
+ const args = ["logcat", "-d", "-t", String(params.maxLines)];
1340
+ if (pid) args.push(`--pid=${pid}`);
1341
+ if (params.tag) {
1342
+ args.push("-s", `${params.tag}:${params.priority}`);
1343
+ }
1344
+ const output = await runAdbCommand(args, timeoutMs);
1345
+ return { content: [{ type: "text", text: output || "Logcat returned no lines." }] };
1360
1346
  }
1361
1347
  );
1362
1348
  server2.registerTool(
1363
1349
  "get-current-activity",
1364
1350
  {
1365
1351
  title: "Get current activity/window focus",
1366
- description: "Inspect current focused app/window via dumpsys window (mCurrentFocus/mFocusedApp). Useful even in single-activity apps to verify top window.",
1352
+ description: "Inspect current focused app/window via dumpsys window.",
1367
1353
  inputSchema: currentActivityInputSchema
1368
1354
  },
1369
1355
  async (params) => {
1370
1356
  const dump = await runAdbCommand(["shell", "dumpsys", "window"], params.timeoutMs);
1371
1357
  const lines = dump.split("\n").filter((line) => line.includes("mCurrentFocus") || line.includes("mFocusedApp"));
1372
1358
  const trimmed = lines.slice(0, 8).join("\n").trim();
1373
- if (!trimmed) {
1374
- return { content: [{ type: "text", text: "No focus info found in dumpsys window." }] };
1375
- }
1376
- return { content: [{ type: "text", text: trimmed }] };
1377
- }
1378
- );
1379
- server2.registerTool(
1380
- "fetch-crash-stacktrace",
1381
- {
1382
- title: "Fetch crash stacktrace (crash buffer)",
1383
- description: "Pull recent crash buffer (-b crash -d -t) optionally filtered by pid resolved from package.",
1384
- inputSchema: crashStackInputSchema
1385
- },
1386
- async (params) => {
1387
- const pid = params.packageName ? await resolvePid(params.packageName, params.timeoutMs) : null;
1388
- const args = ["logcat", "-b", "crash", "-d", "-t", String(params.maxLines)];
1389
- if (pid) {
1390
- args.push(`--pid=${pid}`);
1391
- }
1392
- const output = await runAdbCommand(args, params.timeoutMs);
1393
- if (!output) {
1394
- return { content: [{ type: "text", text: "No crash entries found." }] };
1395
- }
1396
- return { content: [{ type: "text", text: output }] };
1397
- }
1398
- );
1399
- server2.registerTool(
1400
- "check-anr-state",
1401
- {
1402
- title: "Check ANR state (ActivityManager + traces)",
1403
- description: "Check recent ActivityManager ANR logs and tail /data/anr/traces.txt when accessible (best-effort, may require root/debuggable).",
1404
- inputSchema: anrStateInputSchema
1405
- },
1406
- async (params) => {
1407
- const sections = [];
1408
- try {
1409
- const amLogs = await runAdbCommand(
1410
- ["logcat", "-d", "-t", String(params.maxLines), "ActivityManager:E", "*:S"],
1411
- params.timeoutMs
1412
- );
1413
- if (amLogs) {
1414
- sections.push("ActivityManager (recent):\n" + amLogs);
1415
- } else {
1416
- sections.push("ActivityManager (recent): no entries.");
1417
- }
1418
- } catch (error) {
1419
- sections.push(`ActivityManager: ${error.message}`);
1420
- }
1421
- try {
1422
- const stat = await runAdbCommand(["shell", "ls", "-l", "/data/anr/traces.txt"], params.timeoutMs);
1423
- sections.push("traces.txt stat:\n" + stat);
1424
- } catch (error) {
1425
- sections.push(`traces.txt stat: ${error.message}`);
1426
- }
1427
- try {
1428
- const tail = await runAdbCommand(
1429
- ["shell", "tail", "-n", "200", "/data/anr/traces.txt"],
1430
- params.timeoutMs
1431
- );
1432
- if (tail) {
1433
- sections.push("traces.txt tail (200 lines):\n" + tail);
1434
- } else {
1435
- sections.push("traces.txt tail: empty.");
1436
- }
1437
- } catch (error) {
1438
- sections.push(`traces.txt tail: ${error.message}`);
1439
- }
1440
- return { content: [{ type: "text", text: sections.join("\n\n") }] };
1441
- }
1442
- );
1443
- server2.registerTool(
1444
- "clear-logcat-buffer",
1445
- {
1446
- title: "Clear logcat buffer",
1447
- description: "Run adb logcat -c to clear buffers before a new scenario.",
1448
- inputSchema: clearLogcatInputSchema
1449
- },
1450
- async (params) => {
1451
- await runAdbCommand(["logcat", "-c"], params.timeoutMs);
1452
- return { content: [{ type: "text", text: "Cleared logcat buffers." }] };
1359
+ return { content: [{ type: "text", text: trimmed || "No focus info found." }] };
1453
1360
  }
1454
1361
  );
1455
1362
  }
@@ -1511,17 +1418,205 @@ var require_textLengthTool = __commonJS({
1511
1418
  }
1512
1419
  });
1513
1420
 
1421
+ // src/tools/deviceTool.js
1422
+ var require_deviceTool = __commonJS({
1423
+ "src/tools/deviceTool.js"(exports2, module2) {
1424
+ var { execFile } = require("child_process");
1425
+ var { promisify } = require("util");
1426
+ var fs = require("fs");
1427
+ var path = require("path");
1428
+ var os = require("os");
1429
+ var z = require("zod/v4");
1430
+ var execFileAsync = promisify(execFile);
1431
+ var deviceToolInstructions2 = [
1432
+ "Use dump-ui-hierarchy to capture the current screen structure (XML) via uiautomator.",
1433
+ "Use take-screenshot to capture the device screen to a local file (PNG).",
1434
+ "Use inject-input to send interactions like tap, text, swipe, or key events to the device."
1435
+ ].join("\n");
1436
+ var dumpUiSchema = z.object({
1437
+ timeoutMs: z.number().int().min(1e3).max(2e4).default(1e4).describe("Timeout in milliseconds")
1438
+ });
1439
+ var screenshotSchema = z.object({
1440
+ outputPath: z.string().min(1).describe("Local path to save the screenshot (e.g. screenshot.png)"),
1441
+ timeoutMs: z.number().int().min(1e3).max(2e4).default(1e4).describe("Timeout in milliseconds")
1442
+ });
1443
+ var injectInputSchema = z.object({
1444
+ command: z.enum(["tap", "text", "swipe", "keyevent", "back", "home"]).describe("Input command type"),
1445
+ args: z.array(z.string().or(z.number())).optional().describe('Arguments for the command (e.g. [x, y] for tap, ["text"] for text). Optional if elementId/elementText provided.'),
1446
+ elementId: z.string().optional().describe('Find element by resource-id and tap its center (e.g. "com.example:id/button")'),
1447
+ elementText: z.string().optional().describe('Find element by text content and tap its center (e.g. "Login")'),
1448
+ timeoutMs: z.number().int().min(1e3).max(2e4).default(1e4).describe("Timeout in milliseconds")
1449
+ });
1450
+ function getCenterFromBounds(bounds) {
1451
+ const match = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
1452
+ if (!match) return null;
1453
+ const x1 = parseInt(match[1], 10);
1454
+ const y1 = parseInt(match[2], 10);
1455
+ const x2 = parseInt(match[3], 10);
1456
+ const y2 = parseInt(match[4], 10);
1457
+ return {
1458
+ x: Math.round((x1 + x2) / 2),
1459
+ y: Math.round((y1 + y2) / 2)
1460
+ };
1461
+ }
1462
+ async function runAdbCommand(args, timeoutMs, options = {}) {
1463
+ try {
1464
+ const { stdout } = await execFileAsync("adb", args, {
1465
+ timeout: timeoutMs,
1466
+ maxBuffer: 10 * 1024 * 1024,
1467
+ ...options
1468
+ });
1469
+ return stdout;
1470
+ } catch (error) {
1471
+ const stderr = error && typeof error.stderr === "string" ? error.stderr.trim() : "";
1472
+ const message = [`adb ${args.join(" ")} failed`, error.message].filter(Boolean).join(": ");
1473
+ if (stderr) {
1474
+ throw new Error(`${message} | stderr: ${stderr}`);
1475
+ }
1476
+ throw new Error(message);
1477
+ }
1478
+ }
1479
+ async function runAdbCommandBinary(args, timeoutMs) {
1480
+ try {
1481
+ const { stdout } = await execFileAsync("adb", args, {
1482
+ timeout: timeoutMs,
1483
+ encoding: "buffer",
1484
+ maxBuffer: 20 * 1024 * 1024
1485
+ });
1486
+ return stdout;
1487
+ } catch (error) {
1488
+ throw new Error(`adb ${args.join(" ")} failed: ${error.message}`);
1489
+ }
1490
+ }
1491
+ function registerDeviceTool2(server2) {
1492
+ server2.registerTool(
1493
+ "dump-ui-hierarchy",
1494
+ {
1495
+ title: "Dump UI Hierarchy (XML)",
1496
+ description: "Capture the current UI hierarchy as XML using uiautomator.",
1497
+ inputSchema: dumpUiSchema
1498
+ },
1499
+ async (params) => {
1500
+ const devicePath = "/data/local/tmp/mcp_window_dump.xml";
1501
+ await runAdbCommand(["shell", "uiautomator", "dump", devicePath], params.timeoutMs);
1502
+ const content = await runAdbCommand(["shell", "cat", devicePath], params.timeoutMs);
1503
+ return { content: [{ type: "text", text: content.trim() }] };
1504
+ }
1505
+ );
1506
+ server2.registerTool(
1507
+ "take-screenshot",
1508
+ {
1509
+ title: "Take User Screenshot",
1510
+ description: "Capture device screenshot and save to a local file.",
1511
+ inputSchema: screenshotSchema
1512
+ },
1513
+ async (params) => {
1514
+ const buffer = await runAdbCommandBinary(["exec-out", "screencap", "-p"], params.timeoutMs);
1515
+ const absPath = path.resolve(params.outputPath);
1516
+ fs.writeFileSync(absPath, buffer);
1517
+ return { content: [{ type: "text", text: `Screenshot saved to ${absPath}` }] };
1518
+ }
1519
+ );
1520
+ server2.registerTool(
1521
+ "inject-input",
1522
+ {
1523
+ title: "Inject Input Events",
1524
+ description: "Simulate user input interactions (tap, text, swipe, keyevents) or click by UI element.",
1525
+ inputSchema: injectInputSchema
1526
+ },
1527
+ async (params) => {
1528
+ let { command, args } = params;
1529
+ const { elementId, elementText, timeoutMs } = params;
1530
+ args = args || [];
1531
+ if (elementId || elementText) {
1532
+ if (command !== "tap") {
1533
+ throw new Error('elementId/elementText can only be used with command="tap".');
1534
+ }
1535
+ const devicePath = "/data/local/tmp/mcp_input_dump.xml";
1536
+ await runAdbCommand(["shell", "uiautomator", "dump", devicePath], timeoutMs);
1537
+ const xmlContent = await runAdbCommand(["shell", "cat", devicePath], timeoutMs);
1538
+ let targetBounds = null;
1539
+ const nodes = xmlContent.split("<node ");
1540
+ for (const nodeStr of nodes) {
1541
+ let matches = false;
1542
+ if (elementId && nodeStr.includes(`resource-id="${elementId}"`)) matches = true;
1543
+ if (elementText && nodeStr.includes(`text="${elementText}"`)) matches = true;
1544
+ if (matches) {
1545
+ const boundsMatch = nodeStr.match(/bounds="(\[\d+,\d+\]\[\d+,\d+\])"/);
1546
+ if (boundsMatch) {
1547
+ targetBounds = boundsMatch[1];
1548
+ break;
1549
+ }
1550
+ }
1551
+ }
1552
+ if (!targetBounds) {
1553
+ throw new Error(`Could not find element with id="${elementId}" or text="${elementText}" in current UI.`);
1554
+ }
1555
+ const center = getCenterFromBounds(targetBounds);
1556
+ if (!center) {
1557
+ throw new Error(`Invalid bounds found: ${targetBounds}`);
1558
+ }
1559
+ args = [String(center.x), String(center.y)];
1560
+ }
1561
+ let adbArgs = ["shell", "input"];
1562
+ switch (command) {
1563
+ case "tap":
1564
+ if (args.length !== 2) throw new Error("tap requires x and y coordinates (or use elementId/elementText)");
1565
+ adbArgs.push("tap", args[0], args[1]);
1566
+ break;
1567
+ case "text":
1568
+ if (args.length !== 1) throw new Error("text requires a single string argument");
1569
+ let safeText = String(args[0]).replace(/\s/g, "%s");
1570
+ adbArgs.push("text", safeText);
1571
+ break;
1572
+ case "swipe":
1573
+ if (args.length < 4) throw new Error("swipe requires at least x1, y1, x2, y2");
1574
+ adbArgs.push("swipe", ...args);
1575
+ break;
1576
+ case "keyevent":
1577
+ case "back":
1578
+ case "home":
1579
+ if (command === "back") {
1580
+ adbArgs.push("keyevent", "4");
1581
+ } else if (command === "home") {
1582
+ adbArgs.push("keyevent", "3");
1583
+ } else {
1584
+ if (args.length < 1) throw new Error("keyevent requires keycode");
1585
+ adbArgs.push("keyevent", ...args);
1586
+ }
1587
+ break;
1588
+ default:
1589
+ throw new Error(`Unknown command: ${command}`);
1590
+ }
1591
+ await runAdbCommand(adbArgs, timeoutMs);
1592
+ return { content: [{ type: "text", text: `Executed input ${command} ${JSON.stringify(args)}` }] };
1593
+ }
1594
+ );
1595
+ }
1596
+ module2.exports = {
1597
+ registerDeviceTool: registerDeviceTool2,
1598
+ deviceToolInstructions: deviceToolInstructions2
1599
+ };
1600
+ }
1601
+ });
1602
+
1514
1603
  // src/index.js
1515
1604
  var { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
1516
1605
  var { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
1517
1606
  var { registerSvgTool, svgToolInstructions } = require_svgTool();
1518
1607
  var { registerLogcatTool, logcatToolInstructions } = require_logcatTool();
1519
1608
  var { registerTextLengthTool, textLengthToolInstructions } = require_textLengthTool();
1520
- var serverInstructions = [svgToolInstructions, logcatToolInstructions, textLengthToolInstructions].join("\n");
1609
+ var { registerDeviceTool, deviceToolInstructions } = require_deviceTool();
1610
+ var serverInstructions = [
1611
+ svgToolInstructions,
1612
+ logcatToolInstructions,
1613
+ textLengthToolInstructions,
1614
+ deviceToolInstructions
1615
+ ].join("\n");
1521
1616
  var server = new McpServer(
1522
1617
  {
1523
- name: "svg-to-android-drawable",
1524
- version: "1.2.0"
1618
+ name: "android-mcp-toolkit",
1619
+ version: "1.3.0"
1525
1620
  },
1526
1621
  {
1527
1622
  capabilities: { logging: {} },
@@ -1531,6 +1626,7 @@ var server = new McpServer(
1531
1626
  registerSvgTool(server);
1532
1627
  registerLogcatTool(server);
1533
1628
  registerTextLengthTool(server);
1629
+ registerDeviceTool(server);
1534
1630
  async function main() {
1535
1631
  const transport = new StdioServerTransport();
1536
1632
  await server.connect(transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "android-mcp-toolkit",
3
- "version": "1.2.0",
4
- "description": "MCP server that converts SVG into Android VectorDrawable XML with a fast path and caching.",
3
+ "version": "1.3.0",
4
+ "description": "MCP server with useful Android development tools: SVG conversion, Logcat management, and Device automation (dump UI, screenshot, input).",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
7
7
  "start": "node dist/index.js",
@@ -24,11 +24,11 @@
24
24
  "license": "MIT",
25
25
  "type": "commonjs",
26
26
  "dependencies": {
27
- "@modelcontextprotocol/sdk": "^1.24.3",
27
+ "@modelcontextprotocol/sdk": "^1.25.1",
28
28
  "svg-path-bounds": "^1.0.1",
29
- "svgo": "^2.8.0",
29
+ "svgo": "^4.0.0",
30
30
  "svgpath": "^2.5.0",
31
- "zod": "^4.1.13"
31
+ "zod": "^4.2.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "tsup": "^8.3.0",