flutter-skill-mcp 0.2.19 → 0.2.20
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 +37 -8
- package/dart/lib/flutter_skill.dart +199 -83
- package/dart/lib/src/cli/server.dart +95 -35
- package/dart/lib/src/flutter_skill_client.dart +16 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,13 +154,42 @@ flutter-skill.inspect()
|
|
|
154
154
|
|
|
155
155
|
## Installation Methods
|
|
156
156
|
|
|
157
|
-
| Method | Command |
|
|
158
|
-
|
|
159
|
-
| npm | `npm install -g flutter-skill-mcp` |
|
|
160
|
-
| Homebrew | `brew install ai-dashboad/flutter-skill/flutter-skill` |
|
|
161
|
-
|
|
|
162
|
-
|
|
|
163
|
-
|
|
|
157
|
+
| Method | Command | Platform |
|
|
158
|
+
|--------|---------|----------|
|
|
159
|
+
| **npm** | `npm install -g flutter-skill-mcp` | All |
|
|
160
|
+
| **Homebrew** | `brew install ai-dashboad/flutter-skill/flutter-skill` | macOS/Linux |
|
|
161
|
+
| **Docker** | `docker pull ghcr.io/ai-dashboad/flutter-skill` | All |
|
|
162
|
+
| **Snap** | `snap install flutter-skill` | Linux |
|
|
163
|
+
| **Scoop** | `scoop install flutter-skill` | Windows |
|
|
164
|
+
| **Winget** | `winget install AIDashboard.FlutterSkill` | Windows |
|
|
165
|
+
| **pub.dev** | `dart pub global activate flutter_skill` | All |
|
|
166
|
+
| **VSCode** | Extensions → "Flutter Skill" | All |
|
|
167
|
+
| **IntelliJ** | Plugins → "Flutter Skill" | All |
|
|
168
|
+
| **Devcontainer** | See below | All |
|
|
169
|
+
|
|
170
|
+
### Docker
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Run MCP server
|
|
174
|
+
docker run --rm -it ghcr.io/ai-dashboad/flutter-skill server
|
|
175
|
+
|
|
176
|
+
# Or use in docker-compose
|
|
177
|
+
services:
|
|
178
|
+
flutter-skill:
|
|
179
|
+
image: ghcr.io/ai-dashboad/flutter-skill:latest
|
|
180
|
+
command: ["server"]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Devcontainer Feature
|
|
184
|
+
|
|
185
|
+
Add to your `.devcontainer/devcontainer.json`:
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"features": {
|
|
189
|
+
"ghcr.io/ai-dashboad/flutter-skill/flutter-skill:latest": {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
164
193
|
|
|
165
194
|
### Native Binary Performance
|
|
166
195
|
| Version | Startup Time |
|
|
@@ -189,7 +218,7 @@ flutter-skill launch /path/to/project
|
|
|
189
218
|
1. Add dependency:
|
|
190
219
|
```yaml
|
|
191
220
|
dependencies:
|
|
192
|
-
flutter_skill: ^0.2.
|
|
221
|
+
flutter_skill: ^0.2.20
|
|
193
222
|
```
|
|
194
223
|
|
|
195
224
|
2. Initialize in main.dart:
|
|
@@ -25,7 +25,8 @@ class FlutterSkillBinding {
|
|
|
25
25
|
// ==================== EXISTING EXTENSIONS ====================
|
|
26
26
|
|
|
27
27
|
// 1. Interactive Elements
|
|
28
|
-
developer.registerExtension('ext.flutter.flutter_skill.interactive',
|
|
28
|
+
developer.registerExtension('ext.flutter.flutter_skill.interactive',
|
|
29
|
+
(method, parameters) async {
|
|
29
30
|
try {
|
|
30
31
|
final elements = _findInteractiveElements();
|
|
31
32
|
return developer.ServiceExtensionResponse.result(
|
|
@@ -37,13 +38,17 @@ class FlutterSkillBinding {
|
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
// 2. Tap
|
|
40
|
-
developer.registerExtension('ext.flutter.flutter_skill.tap',
|
|
41
|
+
developer.registerExtension('ext.flutter.flutter_skill.tap',
|
|
42
|
+
(method, parameters) async {
|
|
41
43
|
try {
|
|
42
44
|
final key = parameters['key'];
|
|
43
45
|
final text = parameters['text'];
|
|
44
46
|
final success = await _performTap(key: key, text: text);
|
|
45
47
|
return developer.ServiceExtensionResponse.result(
|
|
46
|
-
jsonEncode({
|
|
48
|
+
jsonEncode({
|
|
49
|
+
'success': success,
|
|
50
|
+
'message': success ? 'Tap successful' : 'Element not found'
|
|
51
|
+
}),
|
|
47
52
|
);
|
|
48
53
|
} catch (e, stack) {
|
|
49
54
|
return _errorResponse(e, stack);
|
|
@@ -51,7 +56,8 @@ class FlutterSkillBinding {
|
|
|
51
56
|
});
|
|
52
57
|
|
|
53
58
|
// 3. Enter Text
|
|
54
|
-
developer.registerExtension('ext.flutter.flutter_skill.enterText',
|
|
59
|
+
developer.registerExtension('ext.flutter.flutter_skill.enterText',
|
|
60
|
+
(method, parameters) async {
|
|
55
61
|
try {
|
|
56
62
|
final key = parameters['key'];
|
|
57
63
|
final text = parameters['text'];
|
|
@@ -63,7 +69,10 @@ class FlutterSkillBinding {
|
|
|
63
69
|
}
|
|
64
70
|
final success = await _performEnterText(key: key, text: text);
|
|
65
71
|
return developer.ServiceExtensionResponse.result(
|
|
66
|
-
jsonEncode({
|
|
72
|
+
jsonEncode({
|
|
73
|
+
'success': success,
|
|
74
|
+
'message': success ? 'Text entered' : 'TextField not found'
|
|
75
|
+
}),
|
|
67
76
|
);
|
|
68
77
|
} catch (e, stack) {
|
|
69
78
|
return _errorResponse(e, stack);
|
|
@@ -71,13 +80,17 @@ class FlutterSkillBinding {
|
|
|
71
80
|
});
|
|
72
81
|
|
|
73
82
|
// 4. Scroll
|
|
74
|
-
developer.registerExtension('ext.flutter.flutter_skill.scroll',
|
|
83
|
+
developer.registerExtension('ext.flutter.flutter_skill.scroll',
|
|
84
|
+
(method, parameters) async {
|
|
75
85
|
try {
|
|
76
86
|
final key = parameters['key'];
|
|
77
87
|
final text = parameters['text'];
|
|
78
88
|
final success = await _performScroll(key: key, text: text);
|
|
79
89
|
return developer.ServiceExtensionResponse.result(
|
|
80
|
-
jsonEncode({
|
|
90
|
+
jsonEncode({
|
|
91
|
+
'success': success,
|
|
92
|
+
'message': success ? 'Scroll successful' : 'Element not found'
|
|
93
|
+
}),
|
|
81
94
|
);
|
|
82
95
|
} catch (e, stack) {
|
|
83
96
|
return _errorResponse(e, stack);
|
|
@@ -87,18 +100,21 @@ class FlutterSkillBinding {
|
|
|
87
100
|
// ==================== UI INSPECTION EXTENSIONS ====================
|
|
88
101
|
|
|
89
102
|
// 5. Get Widget Tree
|
|
90
|
-
developer.registerExtension('ext.flutter.flutter_skill.getWidgetTree',
|
|
103
|
+
developer.registerExtension('ext.flutter.flutter_skill.getWidgetTree',
|
|
104
|
+
(method, parameters) async {
|
|
91
105
|
try {
|
|
92
106
|
final maxDepth = int.tryParse(parameters['maxDepth'] ?? '10') ?? 10;
|
|
93
107
|
final tree = _getWidgetTree(maxDepth: maxDepth);
|
|
94
|
-
return developer.ServiceExtensionResponse.result(
|
|
108
|
+
return developer.ServiceExtensionResponse.result(
|
|
109
|
+
jsonEncode({'tree': tree}));
|
|
95
110
|
} catch (e, stack) {
|
|
96
111
|
return _errorResponse(e, stack);
|
|
97
112
|
}
|
|
98
113
|
});
|
|
99
114
|
|
|
100
115
|
// 6. Get Widget Properties
|
|
101
|
-
developer.registerExtension('ext.flutter.flutter_skill.getWidgetProperties',
|
|
116
|
+
developer.registerExtension('ext.flutter.flutter_skill.getWidgetProperties',
|
|
117
|
+
(method, parameters) async {
|
|
102
118
|
try {
|
|
103
119
|
final key = parameters['key'];
|
|
104
120
|
if (key == null) {
|
|
@@ -108,24 +124,28 @@ class FlutterSkillBinding {
|
|
|
108
124
|
);
|
|
109
125
|
}
|
|
110
126
|
final properties = _getWidgetProperties(key);
|
|
111
|
-
return developer.ServiceExtensionResponse.result(
|
|
127
|
+
return developer.ServiceExtensionResponse.result(
|
|
128
|
+
jsonEncode({'properties': properties}));
|
|
112
129
|
} catch (e, stack) {
|
|
113
130
|
return _errorResponse(e, stack);
|
|
114
131
|
}
|
|
115
132
|
});
|
|
116
133
|
|
|
117
134
|
// 7. Get Text Content
|
|
118
|
-
developer.registerExtension('ext.flutter.flutter_skill.getTextContent',
|
|
135
|
+
developer.registerExtension('ext.flutter.flutter_skill.getTextContent',
|
|
136
|
+
(method, parameters) async {
|
|
119
137
|
try {
|
|
120
138
|
final textList = _getTextContent();
|
|
121
|
-
return developer.ServiceExtensionResponse.result(
|
|
139
|
+
return developer.ServiceExtensionResponse.result(
|
|
140
|
+
jsonEncode({'texts': textList}));
|
|
122
141
|
} catch (e, stack) {
|
|
123
142
|
return _errorResponse(e, stack);
|
|
124
143
|
}
|
|
125
144
|
});
|
|
126
145
|
|
|
127
146
|
// 8. Find By Type
|
|
128
|
-
developer.registerExtension('ext.flutter.flutter_skill.findByType',
|
|
147
|
+
developer.registerExtension('ext.flutter.flutter_skill.findByType',
|
|
148
|
+
(method, parameters) async {
|
|
129
149
|
try {
|
|
130
150
|
final type = parameters['type'];
|
|
131
151
|
if (type == null) {
|
|
@@ -135,7 +155,8 @@ class FlutterSkillBinding {
|
|
|
135
155
|
);
|
|
136
156
|
}
|
|
137
157
|
final elements = _findByType(type);
|
|
138
|
-
return developer.ServiceExtensionResponse.result(
|
|
158
|
+
return developer.ServiceExtensionResponse.result(
|
|
159
|
+
jsonEncode({'elements': elements}));
|
|
139
160
|
} catch (e, stack) {
|
|
140
161
|
return _errorResponse(e, stack);
|
|
141
162
|
}
|
|
@@ -144,14 +165,19 @@ class FlutterSkillBinding {
|
|
|
144
165
|
// ==================== MORE INTERACTIONS ====================
|
|
145
166
|
|
|
146
167
|
// 9. Long Press
|
|
147
|
-
developer.registerExtension('ext.flutter.flutter_skill.longPress',
|
|
168
|
+
developer.registerExtension('ext.flutter.flutter_skill.longPress',
|
|
169
|
+
(method, parameters) async {
|
|
148
170
|
try {
|
|
149
171
|
final key = parameters['key'];
|
|
150
172
|
final text = parameters['text'];
|
|
151
173
|
final duration = int.tryParse(parameters['duration'] ?? '500') ?? 500;
|
|
152
|
-
final success =
|
|
174
|
+
final success =
|
|
175
|
+
await _performLongPress(key: key, text: text, duration: duration);
|
|
153
176
|
return developer.ServiceExtensionResponse.result(
|
|
154
|
-
jsonEncode({
|
|
177
|
+
jsonEncode({
|
|
178
|
+
'success': success,
|
|
179
|
+
'message': success ? 'Long press successful' : 'Element not found'
|
|
180
|
+
}),
|
|
155
181
|
);
|
|
156
182
|
} catch (e, stack) {
|
|
157
183
|
return _errorResponse(e, stack);
|
|
@@ -159,14 +185,20 @@ class FlutterSkillBinding {
|
|
|
159
185
|
});
|
|
160
186
|
|
|
161
187
|
// 10. Swipe
|
|
162
|
-
developer.registerExtension('ext.flutter.flutter_skill.swipe',
|
|
188
|
+
developer.registerExtension('ext.flutter.flutter_skill.swipe',
|
|
189
|
+
(method, parameters) async {
|
|
163
190
|
try {
|
|
164
191
|
final direction = parameters['direction'] ?? 'up';
|
|
165
|
-
final distance =
|
|
192
|
+
final distance =
|
|
193
|
+
double.tryParse(parameters['distance'] ?? '300') ?? 300;
|
|
166
194
|
final key = parameters['key'];
|
|
167
|
-
final success = await _performSwipe(
|
|
195
|
+
final success = await _performSwipe(
|
|
196
|
+
direction: direction, distance: distance, key: key);
|
|
168
197
|
return developer.ServiceExtensionResponse.result(
|
|
169
|
-
jsonEncode({
|
|
198
|
+
jsonEncode({
|
|
199
|
+
'success': success,
|
|
200
|
+
'message': success ? 'Swipe successful' : 'Swipe failed'
|
|
201
|
+
}),
|
|
170
202
|
);
|
|
171
203
|
} catch (e, stack) {
|
|
172
204
|
return _errorResponse(e, stack);
|
|
@@ -174,13 +206,17 @@ class FlutterSkillBinding {
|
|
|
174
206
|
});
|
|
175
207
|
|
|
176
208
|
// 11. Drag
|
|
177
|
-
developer.registerExtension('ext.flutter.flutter_skill.drag',
|
|
209
|
+
developer.registerExtension('ext.flutter.flutter_skill.drag',
|
|
210
|
+
(method, parameters) async {
|
|
178
211
|
try {
|
|
179
212
|
final fromKey = parameters['fromKey'];
|
|
180
213
|
final toKey = parameters['toKey'];
|
|
181
214
|
final success = await _performDrag(fromKey: fromKey, toKey: toKey);
|
|
182
215
|
return developer.ServiceExtensionResponse.result(
|
|
183
|
-
jsonEncode({
|
|
216
|
+
jsonEncode({
|
|
217
|
+
'success': success,
|
|
218
|
+
'message': success ? 'Drag successful' : 'Drag failed'
|
|
219
|
+
}),
|
|
184
220
|
);
|
|
185
221
|
} catch (e, stack) {
|
|
186
222
|
return _errorResponse(e, stack);
|
|
@@ -188,13 +224,17 @@ class FlutterSkillBinding {
|
|
|
188
224
|
});
|
|
189
225
|
|
|
190
226
|
// 12. Double Tap
|
|
191
|
-
developer.registerExtension('ext.flutter.flutter_skill.doubleTap',
|
|
227
|
+
developer.registerExtension('ext.flutter.flutter_skill.doubleTap',
|
|
228
|
+
(method, parameters) async {
|
|
192
229
|
try {
|
|
193
230
|
final key = parameters['key'];
|
|
194
231
|
final text = parameters['text'];
|
|
195
232
|
final success = await _performDoubleTap(key: key, text: text);
|
|
196
233
|
return developer.ServiceExtensionResponse.result(
|
|
197
|
-
jsonEncode({
|
|
234
|
+
jsonEncode({
|
|
235
|
+
'success': success,
|
|
236
|
+
'message': success ? 'Double tap successful' : 'Element not found'
|
|
237
|
+
}),
|
|
198
238
|
);
|
|
199
239
|
} catch (e, stack) {
|
|
200
240
|
return _errorResponse(e, stack);
|
|
@@ -204,7 +244,8 @@ class FlutterSkillBinding {
|
|
|
204
244
|
// ==================== STATE & VALIDATION ====================
|
|
205
245
|
|
|
206
246
|
// 13. Get Text Value
|
|
207
|
-
developer.registerExtension('ext.flutter.flutter_skill.getTextValue',
|
|
247
|
+
developer.registerExtension('ext.flutter.flutter_skill.getTextValue',
|
|
248
|
+
(method, parameters) async {
|
|
208
249
|
try {
|
|
209
250
|
final key = parameters['key'];
|
|
210
251
|
if (key == null) {
|
|
@@ -214,14 +255,16 @@ class FlutterSkillBinding {
|
|
|
214
255
|
);
|
|
215
256
|
}
|
|
216
257
|
final value = _getTextFieldValue(key);
|
|
217
|
-
return developer.ServiceExtensionResponse.result(
|
|
258
|
+
return developer.ServiceExtensionResponse.result(
|
|
259
|
+
jsonEncode({'value': value}));
|
|
218
260
|
} catch (e, stack) {
|
|
219
261
|
return _errorResponse(e, stack);
|
|
220
262
|
}
|
|
221
263
|
});
|
|
222
264
|
|
|
223
265
|
// 14. Get Checkbox State
|
|
224
|
-
developer.registerExtension('ext.flutter.flutter_skill.getCheckboxState',
|
|
266
|
+
developer.registerExtension('ext.flutter.flutter_skill.getCheckboxState',
|
|
267
|
+
(method, parameters) async {
|
|
225
268
|
try {
|
|
226
269
|
final key = parameters['key'];
|
|
227
270
|
if (key == null) {
|
|
@@ -231,14 +274,16 @@ class FlutterSkillBinding {
|
|
|
231
274
|
);
|
|
232
275
|
}
|
|
233
276
|
final state = _getCheckboxState(key);
|
|
234
|
-
return developer.ServiceExtensionResponse.result(
|
|
277
|
+
return developer.ServiceExtensionResponse.result(
|
|
278
|
+
jsonEncode({'checked': state}));
|
|
235
279
|
} catch (e, stack) {
|
|
236
280
|
return _errorResponse(e, stack);
|
|
237
281
|
}
|
|
238
282
|
});
|
|
239
283
|
|
|
240
284
|
// 15. Get Slider Value
|
|
241
|
-
developer.registerExtension('ext.flutter.flutter_skill.getSliderValue',
|
|
285
|
+
developer.registerExtension('ext.flutter.flutter_skill.getSliderValue',
|
|
286
|
+
(method, parameters) async {
|
|
242
287
|
try {
|
|
243
288
|
final key = parameters['key'];
|
|
244
289
|
if (key == null) {
|
|
@@ -248,21 +293,27 @@ class FlutterSkillBinding {
|
|
|
248
293
|
);
|
|
249
294
|
}
|
|
250
295
|
final value = _getSliderValue(key);
|
|
251
|
-
return developer.ServiceExtensionResponse.result(
|
|
296
|
+
return developer.ServiceExtensionResponse.result(
|
|
297
|
+
jsonEncode({'value': value}));
|
|
252
298
|
} catch (e, stack) {
|
|
253
299
|
return _errorResponse(e, stack);
|
|
254
300
|
}
|
|
255
301
|
});
|
|
256
302
|
|
|
257
303
|
// 16. Wait For Element
|
|
258
|
-
developer.registerExtension('ext.flutter.flutter_skill.waitForElement',
|
|
304
|
+
developer.registerExtension('ext.flutter.flutter_skill.waitForElement',
|
|
305
|
+
(method, parameters) async {
|
|
259
306
|
try {
|
|
260
307
|
final key = parameters['key'];
|
|
261
308
|
final text = parameters['text'];
|
|
262
309
|
final timeout = int.tryParse(parameters['timeout'] ?? '5000') ?? 5000;
|
|
263
|
-
final found =
|
|
310
|
+
final found =
|
|
311
|
+
await _waitForElement(key: key, text: text, timeout: timeout);
|
|
264
312
|
return developer.ServiceExtensionResponse.result(
|
|
265
|
-
jsonEncode({
|
|
313
|
+
jsonEncode({
|
|
314
|
+
'found': found,
|
|
315
|
+
'message': found ? 'Element found' : 'Timeout waiting for element'
|
|
316
|
+
}),
|
|
266
317
|
);
|
|
267
318
|
} catch (e, stack) {
|
|
268
319
|
return _errorResponse(e, stack);
|
|
@@ -270,14 +321,18 @@ class FlutterSkillBinding {
|
|
|
270
321
|
});
|
|
271
322
|
|
|
272
323
|
// 17. Wait For Gone
|
|
273
|
-
developer.registerExtension('ext.flutter.flutter_skill.waitForGone',
|
|
324
|
+
developer.registerExtension('ext.flutter.flutter_skill.waitForGone',
|
|
325
|
+
(method, parameters) async {
|
|
274
326
|
try {
|
|
275
327
|
final key = parameters['key'];
|
|
276
328
|
final text = parameters['text'];
|
|
277
329
|
final timeout = int.tryParse(parameters['timeout'] ?? '5000') ?? 5000;
|
|
278
330
|
final gone = await _waitForGone(key: key, text: text, timeout: timeout);
|
|
279
331
|
return developer.ServiceExtensionResponse.result(
|
|
280
|
-
jsonEncode({
|
|
332
|
+
jsonEncode({
|
|
333
|
+
'gone': gone,
|
|
334
|
+
'message': gone ? 'Element is gone' : 'Element still present'
|
|
335
|
+
}),
|
|
281
336
|
);
|
|
282
337
|
} catch (e, stack) {
|
|
283
338
|
return _errorResponse(e, stack);
|
|
@@ -287,17 +342,20 @@ class FlutterSkillBinding {
|
|
|
287
342
|
// ==================== SCREENSHOT ====================
|
|
288
343
|
|
|
289
344
|
// 18. Screenshot
|
|
290
|
-
developer.registerExtension('ext.flutter.flutter_skill.screenshot',
|
|
345
|
+
developer.registerExtension('ext.flutter.flutter_skill.screenshot',
|
|
346
|
+
(method, parameters) async {
|
|
291
347
|
try {
|
|
292
348
|
final base64Image = await _takeScreenshot();
|
|
293
|
-
return developer.ServiceExtensionResponse.result(
|
|
349
|
+
return developer.ServiceExtensionResponse.result(
|
|
350
|
+
jsonEncode({'image': base64Image}));
|
|
294
351
|
} catch (e, stack) {
|
|
295
352
|
return _errorResponse(e, stack);
|
|
296
353
|
}
|
|
297
354
|
});
|
|
298
355
|
|
|
299
356
|
// 19. Screenshot Element
|
|
300
|
-
developer.registerExtension('ext.flutter.flutter_skill.screenshotElement',
|
|
357
|
+
developer.registerExtension('ext.flutter.flutter_skill.screenshotElement',
|
|
358
|
+
(method, parameters) async {
|
|
301
359
|
try {
|
|
302
360
|
final key = parameters['key'];
|
|
303
361
|
if (key == null) {
|
|
@@ -307,7 +365,8 @@ class FlutterSkillBinding {
|
|
|
307
365
|
);
|
|
308
366
|
}
|
|
309
367
|
final base64Image = await _takeElementScreenshot(key);
|
|
310
|
-
return developer.ServiceExtensionResponse.result(
|
|
368
|
+
return developer.ServiceExtensionResponse.result(
|
|
369
|
+
jsonEncode({'image': base64Image}));
|
|
311
370
|
} catch (e, stack) {
|
|
312
371
|
return _errorResponse(e, stack);
|
|
313
372
|
}
|
|
@@ -316,21 +375,27 @@ class FlutterSkillBinding {
|
|
|
316
375
|
// ==================== NAVIGATION ====================
|
|
317
376
|
|
|
318
377
|
// 20. Get Current Route
|
|
319
|
-
developer.registerExtension('ext.flutter.flutter_skill.getCurrentRoute',
|
|
378
|
+
developer.registerExtension('ext.flutter.flutter_skill.getCurrentRoute',
|
|
379
|
+
(method, parameters) async {
|
|
320
380
|
try {
|
|
321
381
|
final route = _getCurrentRoute();
|
|
322
|
-
return developer.ServiceExtensionResponse.result(
|
|
382
|
+
return developer.ServiceExtensionResponse.result(
|
|
383
|
+
jsonEncode({'route': route}));
|
|
323
384
|
} catch (e, stack) {
|
|
324
385
|
return _errorResponse(e, stack);
|
|
325
386
|
}
|
|
326
387
|
});
|
|
327
388
|
|
|
328
389
|
// 21. Go Back
|
|
329
|
-
developer.registerExtension('ext.flutter.flutter_skill.goBack',
|
|
390
|
+
developer.registerExtension('ext.flutter.flutter_skill.goBack',
|
|
391
|
+
(method, parameters) async {
|
|
330
392
|
try {
|
|
331
393
|
final success = _goBack();
|
|
332
394
|
return developer.ServiceExtensionResponse.result(
|
|
333
|
-
jsonEncode({
|
|
395
|
+
jsonEncode({
|
|
396
|
+
'success': success,
|
|
397
|
+
'message': success ? 'Navigated back' : 'Cannot go back'
|
|
398
|
+
}),
|
|
334
399
|
);
|
|
335
400
|
} catch (e, stack) {
|
|
336
401
|
return _errorResponse(e, stack);
|
|
@@ -338,10 +403,12 @@ class FlutterSkillBinding {
|
|
|
338
403
|
});
|
|
339
404
|
|
|
340
405
|
// 22. Get Navigation Stack
|
|
341
|
-
developer.registerExtension('ext.flutter.flutter_skill.getNavigationStack',
|
|
406
|
+
developer.registerExtension('ext.flutter.flutter_skill.getNavigationStack',
|
|
407
|
+
(method, parameters) async {
|
|
342
408
|
try {
|
|
343
409
|
final stack = _getNavigationStack();
|
|
344
|
-
return developer.ServiceExtensionResponse.result(
|
|
410
|
+
return developer.ServiceExtensionResponse.result(
|
|
411
|
+
jsonEncode({'stack': stack}));
|
|
345
412
|
} catch (e, stack) {
|
|
346
413
|
return _errorResponse(e, stack);
|
|
347
414
|
}
|
|
@@ -350,36 +417,43 @@ class FlutterSkillBinding {
|
|
|
350
417
|
// ==================== DEBUG & LOGS ====================
|
|
351
418
|
|
|
352
419
|
// 23. Get Logs
|
|
353
|
-
developer.registerExtension('ext.flutter.flutter_skill.getLogs',
|
|
420
|
+
developer.registerExtension('ext.flutter.flutter_skill.getLogs',
|
|
421
|
+
(method, parameters) async {
|
|
354
422
|
try {
|
|
355
|
-
return developer.ServiceExtensionResponse.result(
|
|
423
|
+
return developer.ServiceExtensionResponse.result(
|
|
424
|
+
jsonEncode({'logs': _logs}));
|
|
356
425
|
} catch (e, stack) {
|
|
357
426
|
return _errorResponse(e, stack);
|
|
358
427
|
}
|
|
359
428
|
});
|
|
360
429
|
|
|
361
430
|
// 24. Get Errors
|
|
362
|
-
developer.registerExtension('ext.flutter.flutter_skill.getErrors',
|
|
431
|
+
developer.registerExtension('ext.flutter.flutter_skill.getErrors',
|
|
432
|
+
(method, parameters) async {
|
|
363
433
|
try {
|
|
364
|
-
return developer.ServiceExtensionResponse.result(
|
|
434
|
+
return developer.ServiceExtensionResponse.result(
|
|
435
|
+
jsonEncode({'errors': _errors}));
|
|
365
436
|
} catch (e, stack) {
|
|
366
437
|
return _errorResponse(e, stack);
|
|
367
438
|
}
|
|
368
439
|
});
|
|
369
440
|
|
|
370
441
|
// 25. Clear Logs
|
|
371
|
-
developer.registerExtension('ext.flutter.flutter_skill.clearLogs',
|
|
442
|
+
developer.registerExtension('ext.flutter.flutter_skill.clearLogs',
|
|
443
|
+
(method, parameters) async {
|
|
372
444
|
try {
|
|
373
445
|
_logs.clear();
|
|
374
446
|
_errors.clear();
|
|
375
|
-
return developer.ServiceExtensionResponse.result(
|
|
447
|
+
return developer.ServiceExtensionResponse.result(
|
|
448
|
+
jsonEncode({'success': true}));
|
|
376
449
|
} catch (e, stack) {
|
|
377
450
|
return _errorResponse(e, stack);
|
|
378
451
|
}
|
|
379
452
|
});
|
|
380
453
|
|
|
381
454
|
// 26. Get Performance
|
|
382
|
-
developer.registerExtension('ext.flutter.flutter_skill.getPerformance',
|
|
455
|
+
developer.registerExtension('ext.flutter.flutter_skill.getPerformance',
|
|
456
|
+
(method, parameters) async {
|
|
383
457
|
try {
|
|
384
458
|
final perf = _getPerformanceMetrics();
|
|
385
459
|
return developer.ServiceExtensionResponse.result(jsonEncode(perf));
|
|
@@ -400,7 +474,8 @@ class FlutterSkillBinding {
|
|
|
400
474
|
};
|
|
401
475
|
}
|
|
402
476
|
|
|
403
|
-
static developer.ServiceExtensionResponse _errorResponse(
|
|
477
|
+
static developer.ServiceExtensionResponse _errorResponse(
|
|
478
|
+
Object e, StackTrace stack) {
|
|
404
479
|
return developer.ServiceExtensionResponse.error(
|
|
405
480
|
developer.ServiceExtensionResponse.extensionError,
|
|
406
481
|
'$e\n$stack',
|
|
@@ -414,7 +489,8 @@ class FlutterSkillBinding {
|
|
|
414
489
|
void visit(Element element) {
|
|
415
490
|
if (found != null) return;
|
|
416
491
|
final widget = element.widget;
|
|
417
|
-
if (widget.key is ValueKey<String> &&
|
|
492
|
+
if (widget.key is ValueKey<String> &&
|
|
493
|
+
(widget.key as ValueKey<String>).value == key) {
|
|
418
494
|
found = element;
|
|
419
495
|
return;
|
|
420
496
|
}
|
|
@@ -474,7 +550,8 @@ class FlutterSkillBinding {
|
|
|
474
550
|
return false;
|
|
475
551
|
}
|
|
476
552
|
|
|
477
|
-
final center =
|
|
553
|
+
final center =
|
|
554
|
+
renderObject.localToGlobal(renderObject.size.center(Offset.zero));
|
|
478
555
|
_log('Tapping at $center (key: $key, text: $text)');
|
|
479
556
|
|
|
480
557
|
await _dispatchTap(center);
|
|
@@ -486,13 +563,16 @@ class FlutterSkillBinding {
|
|
|
486
563
|
final binding = WidgetsBinding.instance;
|
|
487
564
|
final pointer = _pointerCounter++;
|
|
488
565
|
|
|
489
|
-
binding.handlePointerEvent(
|
|
566
|
+
binding.handlePointerEvent(
|
|
567
|
+
PointerDownEvent(position: position, pointer: pointer));
|
|
490
568
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
491
|
-
binding.handlePointerEvent(
|
|
569
|
+
binding.handlePointerEvent(
|
|
570
|
+
PointerUpEvent(position: position, pointer: pointer));
|
|
492
571
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
493
572
|
}
|
|
494
573
|
|
|
495
|
-
static Future<bool> _performEnterText(
|
|
574
|
+
static Future<bool> _performEnterText(
|
|
575
|
+
{String? key, required String text}) async {
|
|
496
576
|
final element = _findElement(key: key);
|
|
497
577
|
if (element == null) {
|
|
498
578
|
_log('TextField not found (key: $key)');
|
|
@@ -502,7 +582,8 @@ class FlutterSkillBinding {
|
|
|
502
582
|
final renderObject = element.renderObject;
|
|
503
583
|
if (renderObject is! RenderBox) return false;
|
|
504
584
|
|
|
505
|
-
final center =
|
|
585
|
+
final center =
|
|
586
|
+
renderObject.localToGlobal(renderObject.size.center(Offset.zero));
|
|
506
587
|
await _dispatchTap(center);
|
|
507
588
|
await Future.delayed(const Duration(milliseconds: 200));
|
|
508
589
|
|
|
@@ -515,6 +596,7 @@ class FlutterSkillBinding {
|
|
|
515
596
|
}
|
|
516
597
|
e.visitChildren(findEditable);
|
|
517
598
|
}
|
|
599
|
+
|
|
518
600
|
findEditable(element);
|
|
519
601
|
|
|
520
602
|
if (editableTextState != null) {
|
|
@@ -545,7 +627,8 @@ class FlutterSkillBinding {
|
|
|
545
627
|
}
|
|
546
628
|
|
|
547
629
|
try {
|
|
548
|
-
await Scrollable.ensureVisible(element,
|
|
630
|
+
await Scrollable.ensureVisible(element,
|
|
631
|
+
duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
|
|
549
632
|
_log('Scrolled to element (key: $key, text: $text)');
|
|
550
633
|
return true;
|
|
551
634
|
} catch (e) {
|
|
@@ -554,27 +637,32 @@ class FlutterSkillBinding {
|
|
|
554
637
|
}
|
|
555
638
|
}
|
|
556
639
|
|
|
557
|
-
static Future<bool> _performLongPress(
|
|
640
|
+
static Future<bool> _performLongPress(
|
|
641
|
+
{String? key, String? text, int duration = 500}) async {
|
|
558
642
|
final element = _findElement(key: key, text: text);
|
|
559
643
|
if (element == null) return false;
|
|
560
644
|
|
|
561
645
|
final renderObject = element.renderObject;
|
|
562
646
|
if (renderObject is! RenderBox || !renderObject.hasSize) return false;
|
|
563
647
|
|
|
564
|
-
final center =
|
|
648
|
+
final center =
|
|
649
|
+
renderObject.localToGlobal(renderObject.size.center(Offset.zero));
|
|
565
650
|
final binding = WidgetsBinding.instance;
|
|
566
651
|
final pointer = _pointerCounter++;
|
|
567
652
|
|
|
568
|
-
binding.handlePointerEvent(
|
|
653
|
+
binding.handlePointerEvent(
|
|
654
|
+
PointerDownEvent(position: center, pointer: pointer));
|
|
569
655
|
await Future.delayed(Duration(milliseconds: duration));
|
|
570
|
-
binding
|
|
656
|
+
binding
|
|
657
|
+
.handlePointerEvent(PointerUpEvent(position: center, pointer: pointer));
|
|
571
658
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
572
659
|
|
|
573
660
|
_log('Long press completed (key: $key, text: $text)');
|
|
574
661
|
return true;
|
|
575
662
|
}
|
|
576
663
|
|
|
577
|
-
static Future<bool> _performSwipe(
|
|
664
|
+
static Future<bool> _performSwipe(
|
|
665
|
+
{required String direction, double distance = 300, String? key}) async {
|
|
578
666
|
final binding = WidgetsBinding.instance;
|
|
579
667
|
Offset start;
|
|
580
668
|
|
|
@@ -612,13 +700,17 @@ class FlutterSkillBinding {
|
|
|
612
700
|
final pointer = _pointerCounter++;
|
|
613
701
|
final end = start + delta;
|
|
614
702
|
|
|
615
|
-
binding.handlePointerEvent(
|
|
703
|
+
binding.handlePointerEvent(
|
|
704
|
+
PointerDownEvent(position: start, pointer: pointer));
|
|
616
705
|
await Future.delayed(const Duration(milliseconds: 16));
|
|
617
706
|
|
|
618
707
|
const steps = 10;
|
|
619
708
|
for (int i = 1; i <= steps; i++) {
|
|
620
709
|
final current = Offset.lerp(start, end, i / steps)!;
|
|
621
|
-
binding.handlePointerEvent(PointerMoveEvent(
|
|
710
|
+
binding.handlePointerEvent(PointerMoveEvent(
|
|
711
|
+
position: current,
|
|
712
|
+
pointer: pointer,
|
|
713
|
+
delta: delta / steps.toDouble()));
|
|
622
714
|
await Future.delayed(const Duration(milliseconds: 16));
|
|
623
715
|
}
|
|
624
716
|
|
|
@@ -646,13 +738,15 @@ class FlutterSkillBinding {
|
|
|
646
738
|
final binding = WidgetsBinding.instance;
|
|
647
739
|
final pointer = _pointerCounter++;
|
|
648
740
|
|
|
649
|
-
binding.handlePointerEvent(
|
|
741
|
+
binding.handlePointerEvent(
|
|
742
|
+
PointerDownEvent(position: start, pointer: pointer));
|
|
650
743
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
651
744
|
|
|
652
745
|
const steps = 20;
|
|
653
746
|
for (int i = 1; i <= steps; i++) {
|
|
654
747
|
final current = Offset.lerp(start, end, i / steps)!;
|
|
655
|
-
binding.handlePointerEvent(
|
|
748
|
+
binding.handlePointerEvent(
|
|
749
|
+
PointerMoveEvent(position: current, pointer: pointer));
|
|
656
750
|
await Future.delayed(const Duration(milliseconds: 16));
|
|
657
751
|
}
|
|
658
752
|
|
|
@@ -670,7 +764,8 @@ class FlutterSkillBinding {
|
|
|
670
764
|
final renderObject = element.renderObject;
|
|
671
765
|
if (renderObject is! RenderBox || !renderObject.hasSize) return false;
|
|
672
766
|
|
|
673
|
-
final center =
|
|
767
|
+
final center =
|
|
768
|
+
renderObject.localToGlobal(renderObject.size.center(Offset.zero));
|
|
674
769
|
|
|
675
770
|
await _dispatchTap(center);
|
|
676
771
|
await Future.delayed(const Duration(milliseconds: 50));
|
|
@@ -695,8 +790,11 @@ class FlutterSkillBinding {
|
|
|
695
790
|
key = (widget.key as ValueKey<String>).value;
|
|
696
791
|
}
|
|
697
792
|
|
|
698
|
-
if (widget is ElevatedButton ||
|
|
699
|
-
widget is
|
|
793
|
+
if (widget is ElevatedButton ||
|
|
794
|
+
widget is TextButton ||
|
|
795
|
+
widget is OutlinedButton ||
|
|
796
|
+
widget is IconButton ||
|
|
797
|
+
widget is FloatingActionButton) {
|
|
700
798
|
type = 'Button';
|
|
701
799
|
text = _extractTextFrom(element);
|
|
702
800
|
} else if (widget is TextField || widget is TextFormField) {
|
|
@@ -752,7 +850,10 @@ class FlutterSkillBinding {
|
|
|
752
850
|
|
|
753
851
|
final renderObject = element.renderObject;
|
|
754
852
|
if (renderObject is RenderBox && renderObject.hasSize) {
|
|
755
|
-
node['size'] = {
|
|
853
|
+
node['size'] = {
|
|
854
|
+
'width': renderObject.size.width,
|
|
855
|
+
'height': renderObject.size.height
|
|
856
|
+
};
|
|
756
857
|
final offset = renderObject.localToGlobal(Offset.zero);
|
|
757
858
|
node['position'] = {'x': offset.dx, 'y': offset.dy};
|
|
758
859
|
}
|
|
@@ -790,10 +891,15 @@ class FlutterSkillBinding {
|
|
|
790
891
|
|
|
791
892
|
final renderObject = element.renderObject;
|
|
792
893
|
if (renderObject is RenderBox && renderObject.hasSize) {
|
|
793
|
-
props['size'] = {
|
|
894
|
+
props['size'] = {
|
|
895
|
+
'width': renderObject.size.width,
|
|
896
|
+
'height': renderObject.size.height
|
|
897
|
+
};
|
|
794
898
|
final offset = renderObject.localToGlobal(Offset.zero);
|
|
795
899
|
props['position'] = {'x': offset.dx, 'y': offset.dy};
|
|
796
|
-
props['visible'] = renderObject.attached &&
|
|
900
|
+
props['visible'] = renderObject.attached &&
|
|
901
|
+
renderObject.size.width > 0 &&
|
|
902
|
+
renderObject.size.height > 0;
|
|
797
903
|
}
|
|
798
904
|
|
|
799
905
|
if (widget is Text) {
|
|
@@ -864,7 +970,10 @@ class FlutterSkillBinding {
|
|
|
864
970
|
if (renderObject is RenderBox && renderObject.hasSize) {
|
|
865
971
|
final offset = renderObject.localToGlobal(Offset.zero);
|
|
866
972
|
node['position'] = {'x': offset.dx, 'y': offset.dy};
|
|
867
|
-
node['size'] = {
|
|
973
|
+
node['size'] = {
|
|
974
|
+
'width': renderObject.size.width,
|
|
975
|
+
'height': renderObject.size.height
|
|
976
|
+
};
|
|
868
977
|
}
|
|
869
978
|
|
|
870
979
|
results.add(node);
|
|
@@ -897,6 +1006,7 @@ class FlutterSkillBinding {
|
|
|
897
1006
|
}
|
|
898
1007
|
e.visitChildren(findEditable);
|
|
899
1008
|
}
|
|
1009
|
+
|
|
900
1010
|
findEditable(element);
|
|
901
1011
|
|
|
902
1012
|
return editableTextState?.textEditingValue.text;
|
|
@@ -927,7 +1037,8 @@ class FlutterSkillBinding {
|
|
|
927
1037
|
return null;
|
|
928
1038
|
}
|
|
929
1039
|
|
|
930
|
-
static Future<bool> _waitForElement(
|
|
1040
|
+
static Future<bool> _waitForElement(
|
|
1041
|
+
{String? key, String? text, int timeout = 5000}) async {
|
|
931
1042
|
final endTime = DateTime.now().add(Duration(milliseconds: timeout));
|
|
932
1043
|
|
|
933
1044
|
while (DateTime.now().isBefore(endTime)) {
|
|
@@ -939,7 +1050,8 @@ class FlutterSkillBinding {
|
|
|
939
1050
|
return false;
|
|
940
1051
|
}
|
|
941
1052
|
|
|
942
|
-
static Future<bool> _waitForGone(
|
|
1053
|
+
static Future<bool> _waitForGone(
|
|
1054
|
+
{String? key, String? text, int timeout = 5000}) async {
|
|
943
1055
|
final endTime = DateTime.now().add(Duration(milliseconds: timeout));
|
|
944
1056
|
|
|
945
1057
|
while (DateTime.now().isBefore(endTime)) {
|
|
@@ -969,6 +1081,7 @@ class FlutterSkillBinding {
|
|
|
969
1081
|
}
|
|
970
1082
|
obj.visitChildren(findBoundary);
|
|
971
1083
|
}
|
|
1084
|
+
|
|
972
1085
|
renderObject?.visitChildren(findBoundary);
|
|
973
1086
|
|
|
974
1087
|
if (boundary == null) {
|
|
@@ -1024,7 +1137,8 @@ class FlutterSkillBinding {
|
|
|
1024
1137
|
final boundaryBox = current;
|
|
1025
1138
|
|
|
1026
1139
|
// Get element position relative to boundary
|
|
1027
|
-
final offset =
|
|
1140
|
+
final offset =
|
|
1141
|
+
box.localToGlobal(Offset.zero, ancestor: boundaryBox);
|
|
1028
1142
|
final size = box.size;
|
|
1029
1143
|
|
|
1030
1144
|
// Capture the boundary
|
|
@@ -1043,8 +1157,10 @@ class FlutterSkillBinding {
|
|
|
1043
1157
|
);
|
|
1044
1158
|
|
|
1045
1159
|
final picture = recorder.endRecording();
|
|
1046
|
-
final croppedImage =
|
|
1047
|
-
|
|
1160
|
+
final croppedImage =
|
|
1161
|
+
await picture.toImage(size.width.toInt(), size.height.toInt());
|
|
1162
|
+
final byteData =
|
|
1163
|
+
await croppedImage.toByteData(format: ui.ImageByteFormat.png);
|
|
1048
1164
|
if (byteData == null) return null;
|
|
1049
1165
|
return base64Encode(byteData.buffer.asUint8List());
|
|
1050
1166
|
}
|
|
@@ -19,24 +19,35 @@ Future<void> runServer(List<String> args) async {
|
|
|
19
19
|
/// Check pub.dev for newer version
|
|
20
20
|
Future<void> _checkForUpdates() async {
|
|
21
21
|
try {
|
|
22
|
-
final response = await http
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
final response = await http
|
|
23
|
+
.get(
|
|
24
|
+
Uri.parse('https://pub.dev/api/packages/flutter_skill'),
|
|
25
|
+
)
|
|
26
|
+
.timeout(const Duration(seconds: 5));
|
|
25
27
|
|
|
26
28
|
if (response.statusCode == 200) {
|
|
27
29
|
final data = jsonDecode(response.body);
|
|
28
30
|
final latestVersion = data['latest']?['version'] as String?;
|
|
29
31
|
|
|
30
|
-
if (latestVersion != null &&
|
|
32
|
+
if (latestVersion != null &&
|
|
33
|
+
_isNewerVersion(latestVersion, _currentVersion)) {
|
|
31
34
|
stderr.writeln('');
|
|
32
|
-
stderr.writeln(
|
|
33
|
-
|
|
34
|
-
stderr.writeln(
|
|
35
|
-
|
|
36
|
-
stderr.writeln(
|
|
37
|
-
|
|
38
|
-
stderr.writeln(
|
|
39
|
-
|
|
35
|
+
stderr.writeln(
|
|
36
|
+
'╔══════════════════════════════════════════════════════════╗');
|
|
37
|
+
stderr.writeln(
|
|
38
|
+
'║ flutter-skill v$latestVersion available (current: v$_currentVersion)');
|
|
39
|
+
stderr.writeln(
|
|
40
|
+
'║ ║');
|
|
41
|
+
stderr.writeln(
|
|
42
|
+
'║ Update with: ║');
|
|
43
|
+
stderr.writeln(
|
|
44
|
+
'║ dart pub global activate flutter_skill ║');
|
|
45
|
+
stderr.writeln(
|
|
46
|
+
'║ Or: ║');
|
|
47
|
+
stderr.writeln(
|
|
48
|
+
'║ npm update -g flutter-skill-mcp ║');
|
|
49
|
+
stderr.writeln(
|
|
50
|
+
'╚══════════════════════════════════════════════════════════╝');
|
|
40
51
|
stderr.writeln('');
|
|
41
52
|
}
|
|
42
53
|
}
|
|
@@ -64,7 +75,10 @@ class FlutterMcpServer {
|
|
|
64
75
|
Process? _flutterProcess;
|
|
65
76
|
|
|
66
77
|
Future<void> run() async {
|
|
67
|
-
stdin
|
|
78
|
+
stdin
|
|
79
|
+
.transform(utf8.decoder)
|
|
80
|
+
.transform(const LineSplitter())
|
|
81
|
+
.listen((line) async {
|
|
68
82
|
if (line.trim().isEmpty) return;
|
|
69
83
|
try {
|
|
70
84
|
final request = jsonDecode(line);
|
|
@@ -87,7 +101,10 @@ class FlutterMcpServer {
|
|
|
87
101
|
_sendResult(id, {
|
|
88
102
|
"capabilities": {"tools": {}, "resources": {}},
|
|
89
103
|
"protocolVersion": "2024-11-05",
|
|
90
|
-
"serverInfo": {
|
|
104
|
+
"serverInfo": {
|
|
105
|
+
"name": "flutter-skill-mcp",
|
|
106
|
+
"version": _currentVersion
|
|
107
|
+
},
|
|
91
108
|
});
|
|
92
109
|
} else if (method == 'notifications/initialized') {
|
|
93
110
|
// No op
|
|
@@ -119,7 +136,10 @@ class FlutterMcpServer {
|
|
|
119
136
|
"inputSchema": {
|
|
120
137
|
"type": "object",
|
|
121
138
|
"properties": {
|
|
122
|
-
"uri": {
|
|
139
|
+
"uri": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"description": "WebSocket URI (ws://...)"
|
|
142
|
+
},
|
|
123
143
|
},
|
|
124
144
|
"required": ["uri"],
|
|
125
145
|
},
|
|
@@ -130,7 +150,10 @@ class FlutterMcpServer {
|
|
|
130
150
|
"inputSchema": {
|
|
131
151
|
"type": "object",
|
|
132
152
|
"properties": {
|
|
133
|
-
"project_path": {
|
|
153
|
+
"project_path": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"description": "Path to Flutter project"
|
|
156
|
+
},
|
|
134
157
|
"device_id": {"type": "string", "description": "Target device"},
|
|
135
158
|
},
|
|
136
159
|
},
|
|
@@ -148,7 +171,10 @@ class FlutterMcpServer {
|
|
|
148
171
|
"inputSchema": {
|
|
149
172
|
"type": "object",
|
|
150
173
|
"properties": {
|
|
151
|
-
"max_depth": {
|
|
174
|
+
"max_depth": {
|
|
175
|
+
"type": "integer",
|
|
176
|
+
"description": "Maximum tree depth (default: 10)"
|
|
177
|
+
},
|
|
152
178
|
},
|
|
153
179
|
},
|
|
154
180
|
},
|
|
@@ -174,7 +200,10 @@ class FlutterMcpServer {
|
|
|
174
200
|
"inputSchema": {
|
|
175
201
|
"type": "object",
|
|
176
202
|
"properties": {
|
|
177
|
-
"type": {
|
|
203
|
+
"type": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"description": "Widget type name to search"
|
|
206
|
+
},
|
|
178
207
|
},
|
|
179
208
|
"required": ["type"],
|
|
180
209
|
},
|
|
@@ -225,7 +254,10 @@ class FlutterMcpServer {
|
|
|
225
254
|
"properties": {
|
|
226
255
|
"key": {"type": "string", "description": "Widget key"},
|
|
227
256
|
"text": {"type": "string", "description": "Text to find"},
|
|
228
|
-
"duration": {
|
|
257
|
+
"duration": {
|
|
258
|
+
"type": "integer",
|
|
259
|
+
"description": "Duration in ms (default: 500)"
|
|
260
|
+
},
|
|
229
261
|
},
|
|
230
262
|
},
|
|
231
263
|
},
|
|
@@ -246,9 +278,18 @@ class FlutterMcpServer {
|
|
|
246
278
|
"inputSchema": {
|
|
247
279
|
"type": "object",
|
|
248
280
|
"properties": {
|
|
249
|
-
"direction": {
|
|
250
|
-
|
|
251
|
-
|
|
281
|
+
"direction": {
|
|
282
|
+
"type": "string",
|
|
283
|
+
"enum": ["up", "down", "left", "right"]
|
|
284
|
+
},
|
|
285
|
+
"distance": {
|
|
286
|
+
"type": "number",
|
|
287
|
+
"description": "Swipe distance in pixels (default: 300)"
|
|
288
|
+
},
|
|
289
|
+
"key": {
|
|
290
|
+
"type": "string",
|
|
291
|
+
"description": "Start from element (optional)"
|
|
292
|
+
},
|
|
252
293
|
},
|
|
253
294
|
"required": ["direction"],
|
|
254
295
|
},
|
|
@@ -308,7 +349,10 @@ class FlutterMcpServer {
|
|
|
308
349
|
"properties": {
|
|
309
350
|
"key": {"type": "string", "description": "Widget key"},
|
|
310
351
|
"text": {"type": "string", "description": "Text to find"},
|
|
311
|
-
"timeout": {
|
|
352
|
+
"timeout": {
|
|
353
|
+
"type": "integer",
|
|
354
|
+
"description": "Timeout in ms (default: 5000)"
|
|
355
|
+
},
|
|
312
356
|
},
|
|
313
357
|
},
|
|
314
358
|
},
|
|
@@ -320,7 +364,10 @@ class FlutterMcpServer {
|
|
|
320
364
|
"properties": {
|
|
321
365
|
"key": {"type": "string", "description": "Widget key"},
|
|
322
366
|
"text": {"type": "string", "description": "Text to find"},
|
|
323
|
-
"timeout": {
|
|
367
|
+
"timeout": {
|
|
368
|
+
"type": "integer",
|
|
369
|
+
"description": "Timeout in ms (default: 5000)"
|
|
370
|
+
},
|
|
324
371
|
},
|
|
325
372
|
},
|
|
326
373
|
},
|
|
@@ -430,11 +477,15 @@ class FlutterMcpServer {
|
|
|
430
477
|
// Continue even if setup fails
|
|
431
478
|
}
|
|
432
479
|
|
|
433
|
-
_flutterProcess = await Process.start('flutter', processArgs,
|
|
480
|
+
_flutterProcess = await Process.start('flutter', processArgs,
|
|
481
|
+
workingDirectory: projectPath);
|
|
434
482
|
|
|
435
483
|
final completer = Completer<String>();
|
|
436
484
|
|
|
437
|
-
_flutterProcess!.stdout
|
|
485
|
+
_flutterProcess!.stdout
|
|
486
|
+
.transform(utf8.decoder)
|
|
487
|
+
.transform(const LineSplitter())
|
|
488
|
+
.listen((line) {
|
|
438
489
|
if (line.contains('ws://')) {
|
|
439
490
|
final uriRegex = RegExp(r'ws://[a-zA-Z0-9.:/-]+');
|
|
440
491
|
final match = uriRegex.firstMatch(line);
|
|
@@ -443,9 +494,11 @@ class FlutterMcpServer {
|
|
|
443
494
|
_client?.disconnect();
|
|
444
495
|
_client = FlutterSkillClient(uri);
|
|
445
496
|
_client!.connect().then((_) {
|
|
446
|
-
if (!completer.isCompleted)
|
|
497
|
+
if (!completer.isCompleted)
|
|
498
|
+
completer.complete("Launched and connected to $uri");
|
|
447
499
|
}).catchError((e) {
|
|
448
|
-
if (!completer.isCompleted)
|
|
500
|
+
if (!completer.isCompleted)
|
|
501
|
+
completer.completeError("Found URI but failed to connect: $e");
|
|
449
502
|
});
|
|
450
503
|
}
|
|
451
504
|
}
|
|
@@ -510,17 +563,21 @@ class FlutterMcpServer {
|
|
|
510
563
|
// Advanced Actions
|
|
511
564
|
case 'long_press':
|
|
512
565
|
final duration = args['duration'] ?? 500;
|
|
513
|
-
final success = await _client!.longPress(
|
|
566
|
+
final success = await _client!.longPress(
|
|
567
|
+
key: args['key'], text: args['text'], duration: duration);
|
|
514
568
|
return success ? "Long pressed" : "Long press failed";
|
|
515
569
|
case 'double_tap':
|
|
516
|
-
final success =
|
|
570
|
+
final success =
|
|
571
|
+
await _client!.doubleTap(key: args['key'], text: args['text']);
|
|
517
572
|
return success ? "Double tapped" : "Double tap failed";
|
|
518
573
|
case 'swipe':
|
|
519
574
|
final distance = (args['distance'] ?? 300).toDouble();
|
|
520
|
-
final success = await _client!.swipe(
|
|
575
|
+
final success = await _client!.swipe(
|
|
576
|
+
direction: args['direction'], distance: distance, key: args['key']);
|
|
521
577
|
return success ? "Swiped ${args['direction']}" : "Swipe failed";
|
|
522
578
|
case 'drag':
|
|
523
|
-
final success = await _client
|
|
579
|
+
final success = await _client!
|
|
580
|
+
.drag(fromKey: args['from_key'], toKey: args['to_key']);
|
|
524
581
|
return success ? "Dragged" : "Drag failed";
|
|
525
582
|
|
|
526
583
|
// State & Validation
|
|
@@ -532,11 +589,13 @@ class FlutterMcpServer {
|
|
|
532
589
|
return await _client!.getSliderValue(args['key']);
|
|
533
590
|
case 'wait_for_element':
|
|
534
591
|
final timeout = args['timeout'] ?? 5000;
|
|
535
|
-
final found = await _client!.waitForElement(
|
|
592
|
+
final found = await _client!.waitForElement(
|
|
593
|
+
key: args['key'], text: args['text'], timeout: timeout);
|
|
536
594
|
return {"found": found};
|
|
537
595
|
case 'wait_for_gone':
|
|
538
596
|
final timeout = args['timeout'] ?? 5000;
|
|
539
|
-
final gone = await _client!.waitForGone(
|
|
597
|
+
final gone = await _client!.waitForGone(
|
|
598
|
+
key: args['key'], text: args['text'], timeout: timeout);
|
|
540
599
|
return {"gone": gone};
|
|
541
600
|
|
|
542
601
|
// Screenshot
|
|
@@ -574,7 +633,8 @@ class FlutterMcpServer {
|
|
|
574
633
|
|
|
575
634
|
void _requireConnection() {
|
|
576
635
|
if (_client == null || !_client!.isConnected) {
|
|
577
|
-
throw Exception(
|
|
636
|
+
throw Exception(
|
|
637
|
+
"Not connected. Call 'connect_app' or 'launch_app' first.");
|
|
578
638
|
}
|
|
579
639
|
}
|
|
580
640
|
|
|
@@ -29,7 +29,8 @@ class FlutterSkillClient {
|
|
|
29
29
|
_isolateId = null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
Future<Map<String, dynamic>> _call(String method,
|
|
32
|
+
Future<Map<String, dynamic>> _call(String method,
|
|
33
|
+
[Map<String, dynamic>? args]) async {
|
|
33
34
|
if (_service == null || _isolateId == null) {
|
|
34
35
|
throw Exception('Not connected');
|
|
35
36
|
}
|
|
@@ -88,7 +89,8 @@ class FlutterSkillClient {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
Future<Map<String, dynamic>?> getWidgetProperties(String key) async {
|
|
91
|
-
final result =
|
|
92
|
+
final result =
|
|
93
|
+
await _call('ext.flutter.flutter_skill.getWidgetProperties', {
|
|
92
94
|
'key': key,
|
|
93
95
|
});
|
|
94
96
|
return result['properties'];
|
|
@@ -108,7 +110,8 @@ class FlutterSkillClient {
|
|
|
108
110
|
|
|
109
111
|
// ==================== MORE INTERACTIONS ====================
|
|
110
112
|
|
|
111
|
-
Future<bool> longPress(
|
|
113
|
+
Future<bool> longPress(
|
|
114
|
+
{String? key, String? text, int duration = 500}) async {
|
|
112
115
|
final result = await _call('ext.flutter.flutter_skill.longPress', {
|
|
113
116
|
if (key != null) 'key': key,
|
|
114
117
|
if (text != null) 'text': text,
|
|
@@ -117,7 +120,8 @@ class FlutterSkillClient {
|
|
|
117
120
|
return result['success'] == true;
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
Future<bool> swipe(
|
|
123
|
+
Future<bool> swipe(
|
|
124
|
+
{required String direction, double distance = 300, String? key}) async {
|
|
121
125
|
final result = await _call('ext.flutter.flutter_skill.swipe', {
|
|
122
126
|
'direction': direction,
|
|
123
127
|
'distance': distance.toString(),
|
|
@@ -165,7 +169,8 @@ class FlutterSkillClient {
|
|
|
165
169
|
return result['value']?.toDouble();
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
Future<bool> waitForElement(
|
|
172
|
+
Future<bool> waitForElement(
|
|
173
|
+
{String? key, String? text, int timeout = 5000}) async {
|
|
169
174
|
final result = await _call('ext.flutter.flutter_skill.waitForElement', {
|
|
170
175
|
if (key != null) 'key': key,
|
|
171
176
|
if (text != null) 'text': text,
|
|
@@ -174,7 +179,8 @@ class FlutterSkillClient {
|
|
|
174
179
|
return result['found'] == true;
|
|
175
180
|
}
|
|
176
181
|
|
|
177
|
-
Future<bool> waitForGone(
|
|
182
|
+
Future<bool> waitForGone(
|
|
183
|
+
{String? key, String? text, int timeout = 5000}) async {
|
|
178
184
|
final result = await _call('ext.flutter.flutter_skill.waitForGone', {
|
|
179
185
|
if (key != null) 'key': key,
|
|
180
186
|
if (text != null) 'text': text,
|
|
@@ -252,8 +258,10 @@ class FlutterSkillClient {
|
|
|
252
258
|
|
|
253
259
|
Future<Map<String, dynamic>> getLayoutTree() async {
|
|
254
260
|
try {
|
|
255
|
-
final groupName =
|
|
256
|
-
|
|
261
|
+
final groupName =
|
|
262
|
+
'flutter_skill_${DateTime.now().millisecondsSinceEpoch}';
|
|
263
|
+
final result =
|
|
264
|
+
await _call('ext.flutter.inspector.getRootWidgetSummaryTree', {
|
|
257
265
|
'objectGroup': groupName,
|
|
258
266
|
});
|
|
259
267
|
return result;
|