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.
- package/dist/index.js +433 -337
- 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 } =
|
|
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,158 +1287,76 @@ 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
|
-
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
|
|
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: "
|
|
1524
|
-
version: "1.
|
|
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.
|
|
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",
|