replicant-mcp 1.1.0 → 1.2.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 +3 -0
- package/dist/adapters/ui-automator.d.ts +30 -0
- package/dist/adapters/ui-automator.js +109 -5
- package/dist/server.js +1 -1
- package/dist/services/config.js +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/scaling.d.ts +29 -0
- package/dist/services/scaling.js +49 -0
- package/dist/tools/ui.d.ts +10 -0
- package/dist/tools/ui.js +13 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/config.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -63,6 +63,9 @@ replicant-mcp wraps all of this into a clean interface that AI can understand an
|
|
|
63
63
|
| **Developer Experience** | Simplified tool authoring with `defineTool()` helper | Future |
|
|
64
64
|
| | Auto-generate JSON schema from Zod via `zod-to-json-schema` | Future |
|
|
65
65
|
| | Convention-based tool auto-discovery (no manual wiring) | Future |
|
|
66
|
+
| **Screenshot Scaling** | Auto-resize screenshots to prevent API context limits | Planned |
|
|
67
|
+
| | Transparent coordinate conversion (image ↔ device space) | Planned |
|
|
68
|
+
| | Raw mode for external context management | Planned |
|
|
66
69
|
|
|
67
70
|
---
|
|
68
71
|
|
|
@@ -14,18 +14,48 @@ export interface CurrentApp {
|
|
|
14
14
|
export interface ScreenshotOptions {
|
|
15
15
|
localPath?: string;
|
|
16
16
|
inline?: boolean;
|
|
17
|
+
maxDimension?: number;
|
|
18
|
+
raw?: boolean;
|
|
17
19
|
}
|
|
18
20
|
export interface ScreenshotResult {
|
|
19
21
|
mode: "file" | "inline";
|
|
20
22
|
path?: string;
|
|
21
23
|
base64?: string;
|
|
22
24
|
sizeBytes?: number;
|
|
25
|
+
device?: {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
};
|
|
29
|
+
image?: {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
};
|
|
33
|
+
scaleFactor?: number;
|
|
34
|
+
warning?: string;
|
|
23
35
|
}
|
|
24
36
|
export type FindWithOcrResult = FindWithFallbacksResult;
|
|
25
37
|
export type FindOptions = IconFindOptions;
|
|
38
|
+
/**
|
|
39
|
+
* Tracks the current scaling state between device and image coordinates.
|
|
40
|
+
* Updated on every screenshot operation.
|
|
41
|
+
*/
|
|
42
|
+
export interface ScalingState {
|
|
43
|
+
scaleFactor: number;
|
|
44
|
+
deviceWidth: number;
|
|
45
|
+
deviceHeight: number;
|
|
46
|
+
imageWidth: number;
|
|
47
|
+
imageHeight: number;
|
|
48
|
+
}
|
|
26
49
|
export declare class UiAutomatorAdapter {
|
|
27
50
|
private adb;
|
|
51
|
+
private scalingState;
|
|
28
52
|
constructor(adb?: AdbAdapter);
|
|
53
|
+
getScalingState(): ScalingState | null;
|
|
54
|
+
/**
|
|
55
|
+
* Transforms accessibility tree nodes from device space to image space.
|
|
56
|
+
* This ensures bounds/coordinates match the scaled screenshot when scaling is active.
|
|
57
|
+
*/
|
|
58
|
+
private transformTreeToImageSpace;
|
|
29
59
|
dump(deviceId: string): Promise<AccessibilityNode[]>;
|
|
30
60
|
find(deviceId: string, selector: {
|
|
31
61
|
resourceId?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import * as fs from "fs";
|
|
3
|
+
import sharp from "sharp";
|
|
3
4
|
import { AdbAdapter } from "./adb.js";
|
|
4
5
|
import { parseUiDump, findElements, flattenTree } from "../parsers/ui-dump.js";
|
|
5
6
|
import { ReplicantError, ErrorCode } from "../types/index.js";
|
|
@@ -7,6 +8,7 @@ import { extractText, searchText } from "../services/ocr.js";
|
|
|
7
8
|
import { matchIconPattern, matchesResourceId } from "../services/icon-patterns.js";
|
|
8
9
|
import { filterIconCandidates, formatBounds, cropCandidateImage } from "../services/visual-candidates.js";
|
|
9
10
|
import { calculateGridCellBounds, calculatePositionCoordinates, createGridOverlay, POSITION_LABELS, } from "../services/grid.js";
|
|
11
|
+
import { calculateScaleFactor, toImageSpace, toDeviceSpace, boundsToImageSpace } from "../services/scaling.js";
|
|
10
12
|
/**
|
|
11
13
|
* Get default screenshot path in project-relative .replicant/screenshots directory.
|
|
12
14
|
* Creates the directory if it doesn't exist.
|
|
@@ -18,9 +20,35 @@ function getDefaultScreenshotPath() {
|
|
|
18
20
|
}
|
|
19
21
|
export class UiAutomatorAdapter {
|
|
20
22
|
adb;
|
|
23
|
+
scalingState = null;
|
|
21
24
|
constructor(adb = new AdbAdapter()) {
|
|
22
25
|
this.adb = adb;
|
|
23
26
|
}
|
|
27
|
+
// Getter for tests
|
|
28
|
+
getScalingState() {
|
|
29
|
+
return this.scalingState;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Transforms accessibility tree nodes from device space to image space.
|
|
33
|
+
* This ensures bounds/coordinates match the scaled screenshot when scaling is active.
|
|
34
|
+
*/
|
|
35
|
+
transformTreeToImageSpace(nodes) {
|
|
36
|
+
if (!this.scalingState || this.scalingState.scaleFactor === 1.0) {
|
|
37
|
+
return nodes;
|
|
38
|
+
}
|
|
39
|
+
const sf = this.scalingState.scaleFactor;
|
|
40
|
+
return nodes.map((node) => {
|
|
41
|
+
const newBounds = boundsToImageSpace(node.bounds, sf);
|
|
42
|
+
const center = toImageSpace(node.centerX, node.centerY, sf);
|
|
43
|
+
return {
|
|
44
|
+
...node,
|
|
45
|
+
bounds: newBounds,
|
|
46
|
+
centerX: center.x,
|
|
47
|
+
centerY: center.y,
|
|
48
|
+
children: node.children ? this.transformTreeToImageSpace(node.children) : [],
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
24
52
|
async dump(deviceId) {
|
|
25
53
|
// Dump UI hierarchy to device
|
|
26
54
|
await this.adb.shell(deviceId, "uiautomator dump /sdcard/ui-dump.xml");
|
|
@@ -28,14 +56,23 @@ export class UiAutomatorAdapter {
|
|
|
28
56
|
const result = await this.adb.shell(deviceId, "cat /sdcard/ui-dump.xml");
|
|
29
57
|
// Clean up
|
|
30
58
|
await this.adb.shell(deviceId, "rm /sdcard/ui-dump.xml");
|
|
31
|
-
|
|
59
|
+
const tree = parseUiDump(result.stdout);
|
|
60
|
+
return this.transformTreeToImageSpace(tree);
|
|
32
61
|
}
|
|
33
62
|
async find(deviceId, selector) {
|
|
34
63
|
const tree = await this.dump(deviceId);
|
|
35
64
|
return findElements(tree, selector);
|
|
36
65
|
}
|
|
37
66
|
async tap(deviceId, x, y) {
|
|
38
|
-
|
|
67
|
+
// Convert from image space to device space if scaling is active
|
|
68
|
+
let tapX = x;
|
|
69
|
+
let tapY = y;
|
|
70
|
+
if (this.scalingState && this.scalingState.scaleFactor !== 1.0) {
|
|
71
|
+
const converted = toDeviceSpace(x, y, this.scalingState.scaleFactor);
|
|
72
|
+
tapX = converted.x;
|
|
73
|
+
tapY = converted.y;
|
|
74
|
+
}
|
|
75
|
+
await this.adb.shell(deviceId, `input tap ${tapX} ${tapY}`);
|
|
39
76
|
}
|
|
40
77
|
async tapElement(deviceId, element) {
|
|
41
78
|
await this.tap(deviceId, element.centerX, element.centerY);
|
|
@@ -47,6 +84,7 @@ export class UiAutomatorAdapter {
|
|
|
47
84
|
}
|
|
48
85
|
async screenshot(deviceId, options = {}) {
|
|
49
86
|
const remotePath = "/sdcard/replicant-screenshot.png";
|
|
87
|
+
const maxDimension = options.maxDimension ?? 1000;
|
|
50
88
|
// Capture screenshot on device
|
|
51
89
|
const captureResult = await this.adb.shell(deviceId, `screencap -p ${remotePath}`);
|
|
52
90
|
if (captureResult.exitCode !== 0) {
|
|
@@ -54,7 +92,9 @@ export class UiAutomatorAdapter {
|
|
|
54
92
|
}
|
|
55
93
|
try {
|
|
56
94
|
if (options.inline) {
|
|
57
|
-
// Inline mode: return base64
|
|
95
|
+
// Inline mode: return base64 (no scaling support for inline mode)
|
|
96
|
+
// Clear scaling state since inline mode doesn't support coordinate conversion
|
|
97
|
+
this.scalingState = null;
|
|
58
98
|
const base64Result = await this.adb.shell(deviceId, `base64 ${remotePath}`);
|
|
59
99
|
const sizeResult = await this.adb.shell(deviceId, `stat -c%s ${remotePath}`);
|
|
60
100
|
return {
|
|
@@ -64,10 +104,74 @@ export class UiAutomatorAdapter {
|
|
|
64
104
|
};
|
|
65
105
|
}
|
|
66
106
|
else {
|
|
67
|
-
// File mode
|
|
107
|
+
// File mode: pull to local, then optionally scale
|
|
68
108
|
const localPath = options.localPath || getDefaultScreenshotPath();
|
|
69
109
|
await this.adb.pull(deviceId, remotePath, localPath);
|
|
70
|
-
|
|
110
|
+
// Get image dimensions
|
|
111
|
+
const metadata = await sharp(localPath).metadata();
|
|
112
|
+
const deviceWidth = metadata.width;
|
|
113
|
+
const deviceHeight = metadata.height;
|
|
114
|
+
// Handle raw mode
|
|
115
|
+
if (options.raw) {
|
|
116
|
+
this.scalingState = {
|
|
117
|
+
scaleFactor: 1.0,
|
|
118
|
+
deviceWidth,
|
|
119
|
+
deviceHeight,
|
|
120
|
+
imageWidth: deviceWidth,
|
|
121
|
+
imageHeight: deviceHeight,
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
mode: "file",
|
|
125
|
+
path: localPath,
|
|
126
|
+
device: { width: deviceWidth, height: deviceHeight },
|
|
127
|
+
image: { width: deviceWidth, height: deviceHeight },
|
|
128
|
+
scaleFactor: 1.0,
|
|
129
|
+
warning: "Raw mode: no scaling applied. May exceed API limits with multiple images.",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Calculate scale factor
|
|
133
|
+
const scaleFactor = calculateScaleFactor(deviceWidth, deviceHeight, maxDimension);
|
|
134
|
+
if (scaleFactor === 1.0) {
|
|
135
|
+
// No scaling needed
|
|
136
|
+
this.scalingState = {
|
|
137
|
+
scaleFactor: 1.0,
|
|
138
|
+
deviceWidth,
|
|
139
|
+
deviceHeight,
|
|
140
|
+
imageWidth: deviceWidth,
|
|
141
|
+
imageHeight: deviceHeight,
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
mode: "file",
|
|
145
|
+
path: localPath,
|
|
146
|
+
device: { width: deviceWidth, height: deviceHeight },
|
|
147
|
+
image: { width: deviceWidth, height: deviceHeight },
|
|
148
|
+
scaleFactor: 1.0,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Scale the image
|
|
152
|
+
const imageWidth = Math.round(deviceWidth / scaleFactor);
|
|
153
|
+
const imageHeight = Math.round(deviceHeight / scaleFactor);
|
|
154
|
+
await sharp(localPath)
|
|
155
|
+
.resize(imageWidth, imageHeight)
|
|
156
|
+
.toFile(localPath + ".tmp");
|
|
157
|
+
// Replace original with scaled version
|
|
158
|
+
const fsPromises = await import("fs/promises");
|
|
159
|
+
await fsPromises.rename(localPath + ".tmp", localPath);
|
|
160
|
+
// Update scaling state
|
|
161
|
+
this.scalingState = {
|
|
162
|
+
scaleFactor,
|
|
163
|
+
deviceWidth,
|
|
164
|
+
deviceHeight,
|
|
165
|
+
imageWidth,
|
|
166
|
+
imageHeight,
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
mode: "file",
|
|
170
|
+
path: localPath,
|
|
171
|
+
device: { width: deviceWidth, height: deviceHeight },
|
|
172
|
+
image: { width: imageWidth, height: imageHeight },
|
|
173
|
+
scaleFactor,
|
|
174
|
+
};
|
|
71
175
|
}
|
|
72
176
|
}
|
|
73
177
|
finally {
|
package/dist/server.js
CHANGED
|
@@ -40,7 +40,7 @@ Tool mapping:
|
|
|
40
40
|
- Emulator control → emulator-device (not \`emulator\` CLI)
|
|
41
41
|
- Builds → gradle-build (not \`./gradlew\`)
|
|
42
42
|
- Tests → gradle-test (not \`./gradlew test\`)
|
|
43
|
-
- UI automation → ui (
|
|
43
|
+
- UI automation → ui (accessibility-first, screenshots auto-scaled to 1000px)
|
|
44
44
|
|
|
45
45
|
Start with \`adb-device list\` to see connected devices.
|
|
46
46
|
Use \`rtfm\` for detailed documentation on any tool.`,
|
package/dist/services/config.js
CHANGED
|
@@ -40,6 +40,7 @@ function mergeUiConfig(defaults, overrides) {
|
|
|
40
40
|
visualModePackages: overrides.visualModePackages ?? defaults.visualModePackages,
|
|
41
41
|
autoFallbackScreenshot: overrides.autoFallbackScreenshot ?? defaults.autoFallbackScreenshot,
|
|
42
42
|
includeBase64: overrides.includeBase64 ?? defaults.includeBase64,
|
|
43
|
+
maxImageDimension: overrides.maxImageDimension ?? defaults.maxImageDimension,
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
/**
|
package/dist/services/index.d.ts
CHANGED
package/dist/services/index.js
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculate the scale factor needed to fit device dimensions within max dimension.
|
|
3
|
+
* Returns 1.0 if no scaling needed.
|
|
4
|
+
*/
|
|
5
|
+
export declare function calculateScaleFactor(deviceWidth: number, deviceHeight: number, maxDimension: number): number;
|
|
6
|
+
export interface Bounds {
|
|
7
|
+
left: number;
|
|
8
|
+
top: number;
|
|
9
|
+
right: number;
|
|
10
|
+
bottom: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Convert device coordinates to image coordinates.
|
|
14
|
+
*/
|
|
15
|
+
export declare function toImageSpace(deviceX: number, deviceY: number, scaleFactor: number): {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Convert image coordinates to device coordinates.
|
|
21
|
+
*/
|
|
22
|
+
export declare function toDeviceSpace(imageX: number, imageY: number, scaleFactor: number): {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Convert bounds from device space to image space.
|
|
28
|
+
*/
|
|
29
|
+
export declare function boundsToImageSpace(bounds: Bounds, scaleFactor: number): Bounds;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculate the scale factor needed to fit device dimensions within max dimension.
|
|
3
|
+
* Returns 1.0 if no scaling needed.
|
|
4
|
+
*/
|
|
5
|
+
export function calculateScaleFactor(deviceWidth, deviceHeight, maxDimension) {
|
|
6
|
+
const longestSide = Math.max(deviceWidth, deviceHeight);
|
|
7
|
+
if (longestSide <= maxDimension) {
|
|
8
|
+
return 1.0;
|
|
9
|
+
}
|
|
10
|
+
return longestSide / maxDimension;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Convert device coordinates to image coordinates.
|
|
14
|
+
*/
|
|
15
|
+
export function toImageSpace(deviceX, deviceY, scaleFactor) {
|
|
16
|
+
if (scaleFactor === 1.0) {
|
|
17
|
+
return { x: deviceX, y: deviceY };
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
x: Math.round(deviceX / scaleFactor),
|
|
21
|
+
y: Math.round(deviceY / scaleFactor),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Convert image coordinates to device coordinates.
|
|
26
|
+
*/
|
|
27
|
+
export function toDeviceSpace(imageX, imageY, scaleFactor) {
|
|
28
|
+
if (scaleFactor === 1.0) {
|
|
29
|
+
return { x: imageX, y: imageY };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
x: Math.round(imageX * scaleFactor),
|
|
33
|
+
y: Math.round(imageY * scaleFactor),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Convert bounds from device space to image space.
|
|
38
|
+
*/
|
|
39
|
+
export function boundsToImageSpace(bounds, scaleFactor) {
|
|
40
|
+
if (scaleFactor === 1.0) {
|
|
41
|
+
return bounds;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
left: Math.round(bounds.left / scaleFactor),
|
|
45
|
+
top: Math.round(bounds.top / scaleFactor),
|
|
46
|
+
right: Math.round(bounds.right / scaleFactor),
|
|
47
|
+
bottom: Math.round(bounds.bottom / scaleFactor),
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/tools/ui.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export declare const uiInputSchema: z.ZodObject<{
|
|
|
27
27
|
debug: z.ZodOptional<z.ZodBoolean>;
|
|
28
28
|
gridCell: z.ZodOptional<z.ZodNumber>;
|
|
29
29
|
gridPosition: z.ZodOptional<z.ZodNumber>;
|
|
30
|
+
maxDimension: z.ZodOptional<z.ZodNumber>;
|
|
31
|
+
raw: z.ZodOptional<z.ZodBoolean>;
|
|
30
32
|
}, z.core.$strip>;
|
|
31
33
|
export type UiInput = z.infer<typeof uiInputSchema>;
|
|
32
34
|
export declare function handleUiTool(input: UiInput, context: ServerContext, uiConfig?: UiConfig): Promise<Record<string, unknown>>;
|
|
@@ -102,6 +104,14 @@ export declare const uiToolDefinition: {
|
|
|
102
104
|
maximum: number;
|
|
103
105
|
description: string;
|
|
104
106
|
};
|
|
107
|
+
maxDimension: {
|
|
108
|
+
type: string;
|
|
109
|
+
description: string;
|
|
110
|
+
};
|
|
111
|
+
raw: {
|
|
112
|
+
type: string;
|
|
113
|
+
description: string;
|
|
114
|
+
};
|
|
105
115
|
};
|
|
106
116
|
required: string[];
|
|
107
117
|
};
|
package/dist/tools/ui.js
CHANGED
|
@@ -19,6 +19,8 @@ export const uiInputSchema = z.object({
|
|
|
19
19
|
debug: z.boolean().optional(),
|
|
20
20
|
gridCell: z.number().min(1).max(24).optional(),
|
|
21
21
|
gridPosition: z.number().min(1).max(5).optional(),
|
|
22
|
+
maxDimension: z.number().optional(),
|
|
23
|
+
raw: z.boolean().optional(),
|
|
22
24
|
});
|
|
23
25
|
// Store last find results for elementIndex reference
|
|
24
26
|
// Updated to support accessibility, OCR, and grid elements
|
|
@@ -93,6 +95,7 @@ export async function handleUiTool(input, context, uiConfig) {
|
|
|
93
95
|
visualModePackages: [],
|
|
94
96
|
autoFallbackScreenshot: true,
|
|
95
97
|
includeBase64: false,
|
|
98
|
+
maxImageDimension: 1000,
|
|
96
99
|
};
|
|
97
100
|
switch (input.operation) {
|
|
98
101
|
case "dump": {
|
|
@@ -319,6 +322,8 @@ export async function handleUiTool(input, context, uiConfig) {
|
|
|
319
322
|
const result = await context.ui.screenshot(deviceId, {
|
|
320
323
|
localPath: input.localPath,
|
|
321
324
|
inline: input.inline,
|
|
325
|
+
maxDimension: input.maxDimension ?? config.maxImageDimension,
|
|
326
|
+
raw: input.raw,
|
|
322
327
|
});
|
|
323
328
|
return { ...result, deviceId };
|
|
324
329
|
}
|
|
@@ -366,6 +371,14 @@ export const uiToolDefinition = {
|
|
|
366
371
|
debug: { type: "boolean", description: "Include source (accessibility/ocr) and confidence in response" },
|
|
367
372
|
gridCell: { type: "number", minimum: 1, maximum: 24, description: "Grid cell number (1-24) for Tier 5 refinement" },
|
|
368
373
|
gridPosition: { type: "number", minimum: 1, maximum: 5, description: "Position within cell (1=TL, 2=TR, 3=Center, 4=BL, 5=BR)" },
|
|
374
|
+
maxDimension: {
|
|
375
|
+
type: "number",
|
|
376
|
+
description: "Max image dimension in pixels (default: 1000). Higher = better quality, more tokens.",
|
|
377
|
+
},
|
|
378
|
+
raw: {
|
|
379
|
+
type: "boolean",
|
|
380
|
+
description: "Skip scaling, return full device resolution. Warning: may exceed API limits.",
|
|
381
|
+
},
|
|
369
382
|
},
|
|
370
383
|
required: ["operation"],
|
|
371
384
|
},
|
package/dist/types/config.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface UiConfig {
|
|
|
9
9
|
autoFallbackScreenshot: boolean;
|
|
10
10
|
/** Include base64-encoded screenshot in response (default: false) */
|
|
11
11
|
includeBase64: boolean;
|
|
12
|
+
/** Maximum dimension (width or height) for screenshots in pixels (default: 1000) */
|
|
13
|
+
maxImageDimension: number;
|
|
12
14
|
}
|
|
13
15
|
export interface ReplicantConfig {
|
|
14
16
|
ui: UiConfig;
|
package/dist/types/config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "replicant-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Android MCP server for AI-assisted Android development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"check-prereqs": "bash scripts/check-prerequisites.sh",
|
|
19
19
|
"smoke-test": "bash scripts/smoke-test.sh",
|
|
20
20
|
"test:device": "tsx scripts/real-device-test.ts",
|
|
21
|
-
"start": "node dist/index.js",
|
|
21
|
+
"start": "npm run build && node dist/index.js",
|
|
22
22
|
"validate": "npm run build && npm run test -- --run",
|
|
23
23
|
"prepublishOnly": "npm run build && npm test -- --run"
|
|
24
24
|
},
|