android-mcp-toolkit 1.1.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.
- package/README.md +15 -11
- package/dist/index.js +470 -321
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Android MCP Toolkit for AI Agents
|
|
2
2
|
|
|
3
|
-
Small MCP server with
|
|
3
|
+
Small MCP server with three tools:
|
|
4
4
|
- Fast SVG → Android VectorDrawable conversion (cached, file or inline).
|
|
5
5
|
- adb logcat reader with package/pid/tag filters for quick crash triage.
|
|
6
|
+
- Translation length difference estimator to flag risky length deltas before layout breaks.
|
|
6
7
|
|
|
7
8
|
## Why this exists
|
|
8
9
|
**The Mission: Bringing Native Android to the AI Agent Era**
|
|
@@ -72,6 +73,10 @@ While the AI ecosystem flourishes with web-first tools, Android development ofte
|
|
|
72
73
|
- Inputs: `timeoutMs` (default `5000`, max `15000`).
|
|
73
74
|
- Behavior: Runs `adb logcat -c` to clear buffers before a new scenario.
|
|
74
75
|
|
|
76
|
+
- `estimate-text-length-difference`
|
|
77
|
+
- Inputs: `sourceText` (original), `translatedText` (to compare), `tolerancePercent` (default `30`, max `500`).
|
|
78
|
+
- Behavior: Measures grapheme length of both strings, computes percent change, and reports whether it exceeds the tolerance (useful to catch translation length blowups that could break layouts).
|
|
79
|
+
|
|
75
80
|
## Roadmap (planned)
|
|
76
81
|
- Additional MCP tools for Android assets (e.g., batch conversions, validations, optimizers).
|
|
77
82
|
- Optional resource prompts for common Android drawables/templates.
|
|
@@ -83,15 +88,11 @@ While the AI ecosystem flourishes with web-first tools, Android development ofte
|
|
|
83
88
|
|
|
84
89
|
## Quick start
|
|
85
90
|
- `npm install`
|
|
86
|
-
- `npm
|
|
91
|
+
- `npm run build`
|
|
92
|
+
- `node dist/index.js` (stdio MCP server)
|
|
87
93
|
|
|
88
94
|
## Run via npx
|
|
89
|
-
-
|
|
90
|
-
|
|
91
|
-
## Run with Docker
|
|
92
|
-
- Build: `docker build -t svg-to-drawable-mcp .`
|
|
93
|
-
- Run: `docker run --rm -it svg-to-drawable-mcp`
|
|
94
|
-
- The container prints to stdio; point your MCP client at `docker run --rm -i svg-to-drawable-mcp`.
|
|
95
|
+
- Global: `npx android-mcp-toolkit`
|
|
95
96
|
|
|
96
97
|
## Use in Cursor (MCP config)
|
|
97
98
|
Add to your Cursor settings JSON:
|
|
@@ -101,17 +102,20 @@ Add to your Cursor settings JSON:
|
|
|
101
102
|
"figma-desktop": {
|
|
102
103
|
"url": "http://127.0.0.1:3845/mcp"
|
|
103
104
|
},
|
|
104
|
-
"
|
|
105
|
+
"android-mcp-toolkit": {
|
|
105
106
|
"command": "npx",
|
|
106
107
|
"args": [
|
|
107
108
|
"-y",
|
|
108
|
-
"
|
|
109
|
+
"android-mcp-toolkit"
|
|
109
110
|
]
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
```
|
|
114
|
-
|
|
115
|
+
The npx call downloads the published package; no local path required.
|
|
116
|
+
|
|
117
|
+
Quick install via Cursor deep link:
|
|
118
|
+
- `cursor://anysphere.cursor-deeplink/mcp/install?name=android-mcp-toolkit&config=eyJjb21tYW5kIjoibnB4IC15IGFuZHJvaWQtbWNwLXRvb2xraXQifQ%3D%3D`
|
|
115
119
|
|
|
116
120
|
## Examples
|
|
117
121
|
- Input SVG: `sample_svg.svg`
|
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 } =
|
|
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
|
-
|
|
912
|
-
info: {
|
|
913
|
-
input: "string"
|
|
914
|
-
},
|
|
1060
|
+
return {
|
|
915
1061
|
plugins: [
|
|
916
1062
|
{
|
|
917
|
-
name: "
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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: "
|
|
1037
|
-
params: {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
|
1249
|
-
"Use get-
|
|
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
|
|
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
|
|
1260
|
-
maxLines: z.number().int().min(1).max(2e3).default(200).describe("Tail line count
|
|
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,164 +1287,315 @@ var require_logcatTool = __commonJS({
|
|
|
1298
1287
|
}
|
|
1299
1288
|
}
|
|
1300
1289
|
async function resolvePid(packageName, timeoutMs) {
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
}
|
|
1306
|
-
|
|
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
|
-
"
|
|
1300
|
+
"manage-logcat",
|
|
1322
1301
|
{
|
|
1323
|
-
title: "
|
|
1324
|
-
description: "
|
|
1325
|
-
inputSchema:
|
|
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
|
|
1328
|
-
const timeoutMs = params
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1312
|
+
let pid = params.pid;
|
|
1313
|
+
if (!pid && params.packageName) {
|
|
1314
|
+
pid = await resolvePid(params.packageName, timeoutMs);
|
|
1346
1315
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
|
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
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1359
|
+
return { content: [{ type: "text", text: trimmed || "No focus info found." }] };
|
|
1360
|
+
}
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
module2.exports = {
|
|
1364
|
+
registerLogcatTool: registerLogcatTool2,
|
|
1365
|
+
logcatToolInstructions: logcatToolInstructions2
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
// src/tools/textLengthTool.js
|
|
1371
|
+
var require_textLengthTool = __commonJS({
|
|
1372
|
+
"src/tools/textLengthTool.js"(exports2, module2) {
|
|
1373
|
+
var z = require("zod/v4");
|
|
1374
|
+
var textLengthToolInstructions2 = [
|
|
1375
|
+
"Use estimate-text-length-difference to compare original vs translated text lengths and flag large deltas.",
|
|
1376
|
+
"Configure tolerancePercent to set the allowed absolute percentage difference (default 30%).",
|
|
1377
|
+
"The tool reports both lengths, percent change, and whether the change exceeds tolerance."
|
|
1378
|
+
].join("\n");
|
|
1379
|
+
var lengthDiffInputSchema = z.object({
|
|
1380
|
+
sourceText: z.string().min(1).describe("Original text before translation"),
|
|
1381
|
+
translatedText: z.string().min(1).describe("Translated text to compare against the original"),
|
|
1382
|
+
tolerancePercent: z.number().min(1).max(500).default(30).describe("Allowed absolute percent difference between lengths before flagging risk")
|
|
1383
|
+
});
|
|
1384
|
+
function measureLength(text) {
|
|
1385
|
+
return Array.from(text).length;
|
|
1386
|
+
}
|
|
1387
|
+
function registerTextLengthTool2(server2) {
|
|
1388
|
+
server2.registerTool(
|
|
1389
|
+
"estimate-text-length-difference",
|
|
1390
|
+
{
|
|
1391
|
+
title: "Estimate text length difference",
|
|
1392
|
+
description: "Compare original and translated text lengths to detect layout risk; configurable tolerancePercent (default 30%).",
|
|
1393
|
+
inputSchema: lengthDiffInputSchema
|
|
1394
|
+
},
|
|
1395
|
+
async (params) => {
|
|
1396
|
+
const sourceLength = measureLength(params.sourceText);
|
|
1397
|
+
const translatedLength = measureLength(params.translatedText);
|
|
1398
|
+
const delta = translatedLength - sourceLength;
|
|
1399
|
+
const percentChange = sourceLength === 0 ? null : delta / sourceLength * 100;
|
|
1400
|
+
const exceeds = percentChange === null ? translatedLength > 0 : Math.abs(percentChange) > params.tolerancePercent;
|
|
1401
|
+
const direction = delta === 0 ? "no change" : delta > 0 ? "longer" : "shorter";
|
|
1402
|
+
const verdict = percentChange === null && translatedLength === 0 ? "\u2705 Both texts are empty; no length risk." : percentChange === null ? "\u26A0\uFE0F Source length is 0; percent change undefined and translated text is present." : exceeds ? "\u26A0\uFE0F Length difference exceeds tolerance (layout risk likely)." : "\u2705 Length difference within tolerance.";
|
|
1403
|
+
const summary = [
|
|
1404
|
+
verdict,
|
|
1405
|
+
`Source length: ${sourceLength}`,
|
|
1406
|
+
`Translated length: ${translatedLength}`,
|
|
1407
|
+
percentChange === null ? `Change: N/A (source length is 0; direction: ${direction})` : `Change: ${percentChange.toFixed(2)}% (${direction})`,
|
|
1408
|
+
`Tolerance: \xB1${params.tolerancePercent}%`
|
|
1409
|
+
].join("\n");
|
|
1410
|
+
return { content: [{ type: "text", text: summary }] };
|
|
1377
1411
|
}
|
|
1378
1412
|
);
|
|
1413
|
+
}
|
|
1414
|
+
module2.exports = {
|
|
1415
|
+
registerTextLengthTool: registerTextLengthTool2,
|
|
1416
|
+
textLengthToolInstructions: textLengthToolInstructions2
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
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) {
|
|
1379
1492
|
server2.registerTool(
|
|
1380
|
-
"
|
|
1493
|
+
"dump-ui-hierarchy",
|
|
1381
1494
|
{
|
|
1382
|
-
title: "
|
|
1383
|
-
description: "
|
|
1384
|
-
inputSchema:
|
|
1495
|
+
title: "Dump UI Hierarchy (XML)",
|
|
1496
|
+
description: "Capture the current UI hierarchy as XML using uiautomator.",
|
|
1497
|
+
inputSchema: dumpUiSchema
|
|
1385
1498
|
},
|
|
1386
1499
|
async (params) => {
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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 }] };
|
|
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() }] };
|
|
1397
1504
|
}
|
|
1398
1505
|
);
|
|
1399
1506
|
server2.registerTool(
|
|
1400
|
-
"
|
|
1507
|
+
"take-screenshot",
|
|
1401
1508
|
{
|
|
1402
|
-
title: "
|
|
1403
|
-
description: "
|
|
1404
|
-
inputSchema:
|
|
1509
|
+
title: "Take User Screenshot",
|
|
1510
|
+
description: "Capture device screenshot and save to a local file.",
|
|
1511
|
+
inputSchema: screenshotSchema
|
|
1405
1512
|
},
|
|
1406
1513
|
async (params) => {
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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") }] };
|
|
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}` }] };
|
|
1441
1518
|
}
|
|
1442
1519
|
);
|
|
1443
1520
|
server2.registerTool(
|
|
1444
|
-
"
|
|
1521
|
+
"inject-input",
|
|
1445
1522
|
{
|
|
1446
|
-
title: "
|
|
1447
|
-
description: "
|
|
1448
|
-
inputSchema:
|
|
1523
|
+
title: "Inject Input Events",
|
|
1524
|
+
description: "Simulate user input interactions (tap, text, swipe, keyevents) or click by UI element.",
|
|
1525
|
+
inputSchema: injectInputSchema
|
|
1449
1526
|
},
|
|
1450
1527
|
async (params) => {
|
|
1451
|
-
|
|
1452
|
-
|
|
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)}` }] };
|
|
1453
1593
|
}
|
|
1454
1594
|
);
|
|
1455
1595
|
}
|
|
1456
1596
|
module2.exports = {
|
|
1457
|
-
|
|
1458
|
-
|
|
1597
|
+
registerDeviceTool: registerDeviceTool2,
|
|
1598
|
+
deviceToolInstructions: deviceToolInstructions2
|
|
1459
1599
|
};
|
|
1460
1600
|
}
|
|
1461
1601
|
});
|
|
@@ -1465,11 +1605,18 @@ var { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
1465
1605
|
var { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1466
1606
|
var { registerSvgTool, svgToolInstructions } = require_svgTool();
|
|
1467
1607
|
var { registerLogcatTool, logcatToolInstructions } = require_logcatTool();
|
|
1468
|
-
var
|
|
1608
|
+
var { registerTextLengthTool, textLengthToolInstructions } = require_textLengthTool();
|
|
1609
|
+
var { registerDeviceTool, deviceToolInstructions } = require_deviceTool();
|
|
1610
|
+
var serverInstructions = [
|
|
1611
|
+
svgToolInstructions,
|
|
1612
|
+
logcatToolInstructions,
|
|
1613
|
+
textLengthToolInstructions,
|
|
1614
|
+
deviceToolInstructions
|
|
1615
|
+
].join("\n");
|
|
1469
1616
|
var server = new McpServer(
|
|
1470
1617
|
{
|
|
1471
|
-
name: "
|
|
1472
|
-
version: "1.
|
|
1618
|
+
name: "android-mcp-toolkit",
|
|
1619
|
+
version: "1.3.0"
|
|
1473
1620
|
},
|
|
1474
1621
|
{
|
|
1475
1622
|
capabilities: { logging: {} },
|
|
@@ -1478,6 +1625,8 @@ var server = new McpServer(
|
|
|
1478
1625
|
);
|
|
1479
1626
|
registerSvgTool(server);
|
|
1480
1627
|
registerLogcatTool(server);
|
|
1628
|
+
registerTextLengthTool(server);
|
|
1629
|
+
registerDeviceTool(server);
|
|
1481
1630
|
async function main() {
|
|
1482
1631
|
const transport = new StdioServerTransport();
|
|
1483
1632
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-mcp-toolkit",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server
|
|
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.
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
28
28
|
"svg-path-bounds": "^1.0.1",
|
|
29
|
-
"svgo": "^
|
|
29
|
+
"svgo": "^4.0.0",
|
|
30
30
|
"svgpath": "^2.5.0",
|
|
31
|
-
"zod": "^4.1
|
|
31
|
+
"zod": "^4.2.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"tsup": "^8.3.0",
|