foodforthought-cli 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
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.
- ate/__init__.py +1 -1
- ate/mcp_server.py +1167 -125
- {foodforthought_cli-0.2.0.dist-info → foodforthought_cli-0.2.1.dist-info}/METADATA +1 -1
- foodforthought_cli-0.2.1.dist-info/RECORD +9 -0
- foodforthought_cli-0.2.0.dist-info/RECORD +0 -9
- {foodforthought_cli-0.2.0.dist-info → foodforthought_cli-0.2.1.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.0.dist-info → foodforthought_cli-0.2.1.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.0.dist-info → foodforthought_cli-0.2.1.dist-info}/top_level.txt +0 -0
ate/mcp_server.py
CHANGED
|
@@ -29,6 +29,7 @@ import json
|
|
|
29
29
|
import os
|
|
30
30
|
import sys
|
|
31
31
|
from typing import Any, Dict, List, Optional
|
|
32
|
+
from pathlib import Path
|
|
32
33
|
|
|
33
34
|
# Import the existing CLI client
|
|
34
35
|
from ate.cli import ATEClient
|
|
@@ -64,13 +65,16 @@ server = Server("foodforthought")
|
|
|
64
65
|
client = ATEClient()
|
|
65
66
|
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
# ============================================================================
|
|
69
|
+
# Tool Definitions
|
|
70
|
+
# ============================================================================
|
|
71
|
+
|
|
72
|
+
def get_repository_tools() -> List[Tool]:
|
|
73
|
+
"""Repository management tools"""
|
|
70
74
|
return [
|
|
71
75
|
Tool(
|
|
72
76
|
name="ate_init",
|
|
73
|
-
description="Initialize a new FoodforThought repository",
|
|
77
|
+
description="Initialize a new FoodforThought repository for robot skills",
|
|
74
78
|
inputSchema={
|
|
75
79
|
"type": "object",
|
|
76
80
|
"properties": {
|
|
@@ -133,111 +137,708 @@ async def list_tools() -> List[Tool]:
|
|
|
133
137
|
},
|
|
134
138
|
),
|
|
135
139
|
Tool(
|
|
136
|
-
name="
|
|
137
|
-
description="
|
|
140
|
+
name="ate_get_repository",
|
|
141
|
+
description="Get details of a specific repository",
|
|
142
|
+
inputSchema={
|
|
143
|
+
"type": "object",
|
|
144
|
+
"properties": {
|
|
145
|
+
"repo_id": {
|
|
146
|
+
"type": "string",
|
|
147
|
+
"description": "Repository ID",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
"required": ["repo_id"],
|
|
151
|
+
},
|
|
152
|
+
),
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_robot_tools() -> List[Tool]:
|
|
157
|
+
"""Robot profile tools"""
|
|
158
|
+
return [
|
|
159
|
+
Tool(
|
|
160
|
+
name="ate_list_robots",
|
|
161
|
+
description="List available robot profiles",
|
|
162
|
+
inputSchema={
|
|
163
|
+
"type": "object",
|
|
164
|
+
"properties": {
|
|
165
|
+
"search": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"description": "Search query",
|
|
168
|
+
},
|
|
169
|
+
"category": {
|
|
170
|
+
"type": "string",
|
|
171
|
+
"description": "Filter by category",
|
|
172
|
+
},
|
|
173
|
+
"limit": {
|
|
174
|
+
"type": "number",
|
|
175
|
+
"description": "Maximum number of results",
|
|
176
|
+
"default": 20,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
),
|
|
181
|
+
Tool(
|
|
182
|
+
name="ate_get_robot",
|
|
183
|
+
description="Get details of a specific robot profile",
|
|
184
|
+
inputSchema={
|
|
185
|
+
"type": "object",
|
|
186
|
+
"properties": {
|
|
187
|
+
"robot_id": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"description": "Robot profile ID",
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
"required": ["robot_id"],
|
|
193
|
+
},
|
|
194
|
+
),
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_compatibility_tools() -> List[Tool]:
|
|
199
|
+
"""Skill compatibility and adaptation tools"""
|
|
200
|
+
return [
|
|
201
|
+
Tool(
|
|
202
|
+
name="ate_check_transfer",
|
|
203
|
+
description="Check skill transfer compatibility between two robot models",
|
|
204
|
+
inputSchema={
|
|
205
|
+
"type": "object",
|
|
206
|
+
"properties": {
|
|
207
|
+
"source_robot": {
|
|
208
|
+
"type": "string",
|
|
209
|
+
"description": "Source robot model name",
|
|
210
|
+
},
|
|
211
|
+
"target_robot": {
|
|
212
|
+
"type": "string",
|
|
213
|
+
"description": "Target robot model name",
|
|
214
|
+
},
|
|
215
|
+
"skill_id": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"description": "Optional skill ID to check",
|
|
218
|
+
},
|
|
219
|
+
"min_score": {
|
|
220
|
+
"type": "number",
|
|
221
|
+
"description": "Minimum compatibility score threshold (0.0-1.0)",
|
|
222
|
+
"default": 0.0,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
"required": ["source_robot", "target_robot"],
|
|
226
|
+
},
|
|
227
|
+
),
|
|
228
|
+
Tool(
|
|
229
|
+
name="ate_adapt",
|
|
230
|
+
description="Generate adaptation plan for transferring skills between robots",
|
|
231
|
+
inputSchema={
|
|
232
|
+
"type": "object",
|
|
233
|
+
"properties": {
|
|
234
|
+
"source_robot": {
|
|
235
|
+
"type": "string",
|
|
236
|
+
"description": "Source robot model",
|
|
237
|
+
},
|
|
238
|
+
"target_robot": {
|
|
239
|
+
"type": "string",
|
|
240
|
+
"description": "Target robot model",
|
|
241
|
+
},
|
|
242
|
+
"repo_id": {
|
|
243
|
+
"type": "string",
|
|
244
|
+
"description": "Repository ID to adapt",
|
|
245
|
+
},
|
|
246
|
+
"analyze_only": {
|
|
247
|
+
"type": "boolean",
|
|
248
|
+
"description": "Only show compatibility analysis",
|
|
249
|
+
"default": True,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
"required": ["source_robot", "target_robot"],
|
|
253
|
+
},
|
|
254
|
+
),
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_skill_tools() -> List[Tool]:
|
|
259
|
+
"""Skill data tools"""
|
|
260
|
+
return [
|
|
261
|
+
Tool(
|
|
262
|
+
name="ate_pull",
|
|
263
|
+
description="Pull skill data for training in various formats (JSON, RLDS, LeRobot)",
|
|
264
|
+
inputSchema={
|
|
265
|
+
"type": "object",
|
|
266
|
+
"properties": {
|
|
267
|
+
"skill_id": {
|
|
268
|
+
"type": "string",
|
|
269
|
+
"description": "Skill ID to pull",
|
|
270
|
+
},
|
|
271
|
+
"robot": {
|
|
272
|
+
"type": "string",
|
|
273
|
+
"description": "Filter by robot model",
|
|
274
|
+
},
|
|
275
|
+
"format": {
|
|
276
|
+
"type": "string",
|
|
277
|
+
"enum": ["json", "rlds", "lerobot"],
|
|
278
|
+
"description": "Output format",
|
|
279
|
+
"default": "json",
|
|
280
|
+
},
|
|
281
|
+
"output": {
|
|
282
|
+
"type": "string",
|
|
283
|
+
"description": "Output directory",
|
|
284
|
+
"default": "./data",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
"required": ["skill_id"],
|
|
288
|
+
},
|
|
289
|
+
),
|
|
290
|
+
Tool(
|
|
291
|
+
name="ate_upload",
|
|
292
|
+
description="Upload demonstration videos for community labeling",
|
|
293
|
+
inputSchema={
|
|
294
|
+
"type": "object",
|
|
295
|
+
"properties": {
|
|
296
|
+
"path": {
|
|
297
|
+
"type": "string",
|
|
298
|
+
"description": "Path to video file",
|
|
299
|
+
},
|
|
300
|
+
"robot": {
|
|
301
|
+
"type": "string",
|
|
302
|
+
"description": "Robot model in the video",
|
|
303
|
+
},
|
|
304
|
+
"task": {
|
|
305
|
+
"type": "string",
|
|
306
|
+
"description": "Task being demonstrated",
|
|
307
|
+
},
|
|
308
|
+
"project": {
|
|
309
|
+
"type": "string",
|
|
310
|
+
"description": "Project ID to associate with",
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
"required": ["path", "robot", "task"],
|
|
314
|
+
},
|
|
315
|
+
),
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def get_parts_tools() -> List[Tool]:
|
|
320
|
+
"""Hardware parts management tools"""
|
|
321
|
+
return [
|
|
322
|
+
Tool(
|
|
323
|
+
name="ate_parts_list",
|
|
324
|
+
description="List available hardware parts in the catalog",
|
|
325
|
+
inputSchema={
|
|
326
|
+
"type": "object",
|
|
327
|
+
"properties": {
|
|
328
|
+
"category": {
|
|
329
|
+
"type": "string",
|
|
330
|
+
"enum": ["gripper", "sensor", "actuator", "controller",
|
|
331
|
+
"end-effector", "camera", "lidar", "force-torque"],
|
|
332
|
+
"description": "Filter by part category",
|
|
333
|
+
},
|
|
334
|
+
"manufacturer": {
|
|
335
|
+
"type": "string",
|
|
336
|
+
"description": "Filter by manufacturer",
|
|
337
|
+
},
|
|
338
|
+
"search": {
|
|
339
|
+
"type": "string",
|
|
340
|
+
"description": "Search by name or part number",
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
),
|
|
345
|
+
Tool(
|
|
346
|
+
name="ate_parts_check",
|
|
347
|
+
description="Check part compatibility requirements for a skill",
|
|
348
|
+
inputSchema={
|
|
349
|
+
"type": "object",
|
|
350
|
+
"properties": {
|
|
351
|
+
"skill_id": {
|
|
352
|
+
"type": "string",
|
|
353
|
+
"description": "Skill ID to check parts for",
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
"required": ["skill_id"],
|
|
357
|
+
},
|
|
358
|
+
),
|
|
359
|
+
Tool(
|
|
360
|
+
name="ate_parts_require",
|
|
361
|
+
description="Add a part dependency to a skill",
|
|
362
|
+
inputSchema={
|
|
363
|
+
"type": "object",
|
|
364
|
+
"properties": {
|
|
365
|
+
"part_id": {
|
|
366
|
+
"type": "string",
|
|
367
|
+
"description": "Part ID to require",
|
|
368
|
+
},
|
|
369
|
+
"skill_id": {
|
|
370
|
+
"type": "string",
|
|
371
|
+
"description": "Skill ID",
|
|
372
|
+
},
|
|
373
|
+
"version": {
|
|
374
|
+
"type": "string",
|
|
375
|
+
"description": "Minimum version",
|
|
376
|
+
"default": "1.0.0",
|
|
377
|
+
},
|
|
378
|
+
"required": {
|
|
379
|
+
"type": "boolean",
|
|
380
|
+
"description": "Mark as required (not optional)",
|
|
381
|
+
"default": False,
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
"required": ["part_id", "skill_id"],
|
|
385
|
+
},
|
|
386
|
+
),
|
|
387
|
+
Tool(
|
|
388
|
+
name="ate_deps_audit",
|
|
389
|
+
description="Audit and verify all dependencies are compatible for a skill",
|
|
390
|
+
inputSchema={
|
|
391
|
+
"type": "object",
|
|
392
|
+
"properties": {
|
|
393
|
+
"skill_id": {
|
|
394
|
+
"type": "string",
|
|
395
|
+
"description": "Skill ID (optional, uses current repo if not specified)",
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
),
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def get_generate_tools() -> List[Tool]:
|
|
404
|
+
"""Skill generation tools"""
|
|
405
|
+
return [
|
|
406
|
+
Tool(
|
|
407
|
+
name="ate_generate",
|
|
408
|
+
description="Generate skill scaffolding from a natural language task description",
|
|
409
|
+
inputSchema={
|
|
410
|
+
"type": "object",
|
|
411
|
+
"properties": {
|
|
412
|
+
"description": {
|
|
413
|
+
"type": "string",
|
|
414
|
+
"description": "Natural language task description (e.g., 'pick up box and place on pallet')",
|
|
415
|
+
},
|
|
416
|
+
"robot": {
|
|
417
|
+
"type": "string",
|
|
418
|
+
"description": "Target robot model",
|
|
419
|
+
"default": "ur5",
|
|
420
|
+
},
|
|
421
|
+
"output": {
|
|
422
|
+
"type": "string",
|
|
423
|
+
"description": "Output directory for generated files",
|
|
424
|
+
"default": "./new-skill",
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
"required": ["description"],
|
|
428
|
+
},
|
|
429
|
+
),
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def get_workflow_tools() -> List[Tool]:
|
|
434
|
+
"""Workflow composition tools"""
|
|
435
|
+
return [
|
|
436
|
+
Tool(
|
|
437
|
+
name="ate_workflow_validate",
|
|
438
|
+
description="Validate a workflow YAML file for skill composition",
|
|
439
|
+
inputSchema={
|
|
440
|
+
"type": "object",
|
|
441
|
+
"properties": {
|
|
442
|
+
"path": {
|
|
443
|
+
"type": "string",
|
|
444
|
+
"description": "Path to workflow YAML file",
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
"required": ["path"],
|
|
448
|
+
},
|
|
449
|
+
),
|
|
450
|
+
Tool(
|
|
451
|
+
name="ate_workflow_run",
|
|
452
|
+
description="Run a skill workflow/pipeline",
|
|
453
|
+
inputSchema={
|
|
454
|
+
"type": "object",
|
|
455
|
+
"properties": {
|
|
456
|
+
"path": {
|
|
457
|
+
"type": "string",
|
|
458
|
+
"description": "Path to workflow YAML file",
|
|
459
|
+
},
|
|
460
|
+
"sim": {
|
|
461
|
+
"type": "boolean",
|
|
462
|
+
"description": "Run in simulation mode",
|
|
463
|
+
"default": True,
|
|
464
|
+
},
|
|
465
|
+
"dry_run": {
|
|
466
|
+
"type": "boolean",
|
|
467
|
+
"description": "Show execution plan without running",
|
|
468
|
+
"default": False,
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
"required": ["path"],
|
|
472
|
+
},
|
|
473
|
+
),
|
|
474
|
+
Tool(
|
|
475
|
+
name="ate_workflow_export",
|
|
476
|
+
description="Export workflow to other formats (ROS2 launch, JSON)",
|
|
477
|
+
inputSchema={
|
|
478
|
+
"type": "object",
|
|
479
|
+
"properties": {
|
|
480
|
+
"path": {
|
|
481
|
+
"type": "string",
|
|
482
|
+
"description": "Path to workflow YAML file",
|
|
483
|
+
},
|
|
484
|
+
"format": {
|
|
485
|
+
"type": "string",
|
|
486
|
+
"enum": ["ros2", "json"],
|
|
487
|
+
"description": "Export format",
|
|
488
|
+
"default": "ros2",
|
|
489
|
+
},
|
|
490
|
+
"output": {
|
|
491
|
+
"type": "string",
|
|
492
|
+
"description": "Output file path",
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
"required": ["path"],
|
|
496
|
+
},
|
|
497
|
+
),
|
|
498
|
+
]
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def get_team_tools() -> List[Tool]:
|
|
502
|
+
"""Team collaboration tools"""
|
|
503
|
+
return [
|
|
504
|
+
Tool(
|
|
505
|
+
name="ate_team_create",
|
|
506
|
+
description="Create a new team for collaboration",
|
|
507
|
+
inputSchema={
|
|
508
|
+
"type": "object",
|
|
509
|
+
"properties": {
|
|
510
|
+
"name": {
|
|
511
|
+
"type": "string",
|
|
512
|
+
"description": "Team name",
|
|
513
|
+
},
|
|
514
|
+
"description": {
|
|
515
|
+
"type": "string",
|
|
516
|
+
"description": "Team description",
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
"required": ["name"],
|
|
520
|
+
},
|
|
521
|
+
),
|
|
522
|
+
Tool(
|
|
523
|
+
name="ate_team_list",
|
|
524
|
+
description="List teams you belong to",
|
|
525
|
+
inputSchema={
|
|
526
|
+
"type": "object",
|
|
527
|
+
"properties": {},
|
|
528
|
+
},
|
|
529
|
+
),
|
|
530
|
+
Tool(
|
|
531
|
+
name="ate_team_invite",
|
|
532
|
+
description="Invite a user to a team",
|
|
533
|
+
inputSchema={
|
|
534
|
+
"type": "object",
|
|
535
|
+
"properties": {
|
|
536
|
+
"email": {
|
|
537
|
+
"type": "string",
|
|
538
|
+
"description": "Email of user to invite",
|
|
539
|
+
},
|
|
540
|
+
"team": {
|
|
541
|
+
"type": "string",
|
|
542
|
+
"description": "Team slug",
|
|
543
|
+
},
|
|
544
|
+
"role": {
|
|
545
|
+
"type": "string",
|
|
546
|
+
"enum": ["owner", "admin", "member", "viewer"],
|
|
547
|
+
"description": "Role to assign",
|
|
548
|
+
"default": "member",
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
"required": ["email", "team"],
|
|
552
|
+
},
|
|
553
|
+
),
|
|
554
|
+
Tool(
|
|
555
|
+
name="ate_team_share",
|
|
556
|
+
description="Share a skill with a team",
|
|
557
|
+
inputSchema={
|
|
558
|
+
"type": "object",
|
|
559
|
+
"properties": {
|
|
560
|
+
"skill_id": {
|
|
561
|
+
"type": "string",
|
|
562
|
+
"description": "Skill ID to share",
|
|
563
|
+
},
|
|
564
|
+
"team": {
|
|
565
|
+
"type": "string",
|
|
566
|
+
"description": "Team slug",
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
"required": ["skill_id", "team"],
|
|
570
|
+
},
|
|
571
|
+
),
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def get_data_tools() -> List[Tool]:
|
|
576
|
+
"""Dataset management tools"""
|
|
577
|
+
return [
|
|
578
|
+
Tool(
|
|
579
|
+
name="ate_data_upload",
|
|
580
|
+
description="Upload sensor data or demonstration logs for a skill",
|
|
581
|
+
inputSchema={
|
|
582
|
+
"type": "object",
|
|
583
|
+
"properties": {
|
|
584
|
+
"path": {
|
|
585
|
+
"type": "string",
|
|
586
|
+
"description": "Path to data directory or file",
|
|
587
|
+
},
|
|
588
|
+
"skill": {
|
|
589
|
+
"type": "string",
|
|
590
|
+
"description": "Associated skill ID",
|
|
591
|
+
},
|
|
592
|
+
"stage": {
|
|
593
|
+
"type": "string",
|
|
594
|
+
"enum": ["raw", "annotated", "skill-abstracted", "production"],
|
|
595
|
+
"description": "Data stage",
|
|
596
|
+
"default": "raw",
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
"required": ["path", "skill"],
|
|
600
|
+
},
|
|
601
|
+
),
|
|
602
|
+
Tool(
|
|
603
|
+
name="ate_data_list",
|
|
604
|
+
description="List datasets for a skill",
|
|
605
|
+
inputSchema={
|
|
606
|
+
"type": "object",
|
|
607
|
+
"properties": {
|
|
608
|
+
"skill": {
|
|
609
|
+
"type": "string",
|
|
610
|
+
"description": "Filter by skill ID",
|
|
611
|
+
},
|
|
612
|
+
"stage": {
|
|
613
|
+
"type": "string",
|
|
614
|
+
"description": "Filter by data stage",
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
),
|
|
619
|
+
Tool(
|
|
620
|
+
name="ate_data_promote",
|
|
621
|
+
description="Promote a dataset to the next stage in the pipeline",
|
|
622
|
+
inputSchema={
|
|
623
|
+
"type": "object",
|
|
624
|
+
"properties": {
|
|
625
|
+
"dataset_id": {
|
|
626
|
+
"type": "string",
|
|
627
|
+
"description": "Dataset ID",
|
|
628
|
+
},
|
|
629
|
+
"to_stage": {
|
|
630
|
+
"type": "string",
|
|
631
|
+
"enum": ["annotated", "skill-abstracted", "production"],
|
|
632
|
+
"description": "Target stage",
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
"required": ["dataset_id", "to_stage"],
|
|
636
|
+
},
|
|
637
|
+
),
|
|
638
|
+
Tool(
|
|
639
|
+
name="ate_data_export",
|
|
640
|
+
description="Export a dataset in various formats",
|
|
641
|
+
inputSchema={
|
|
642
|
+
"type": "object",
|
|
643
|
+
"properties": {
|
|
644
|
+
"dataset_id": {
|
|
645
|
+
"type": "string",
|
|
646
|
+
"description": "Dataset ID",
|
|
647
|
+
},
|
|
648
|
+
"format": {
|
|
649
|
+
"type": "string",
|
|
650
|
+
"enum": ["json", "rlds", "lerobot", "hdf5"],
|
|
651
|
+
"description": "Export format",
|
|
652
|
+
"default": "rlds",
|
|
653
|
+
},
|
|
654
|
+
"output": {
|
|
655
|
+
"type": "string",
|
|
656
|
+
"description": "Output directory",
|
|
657
|
+
"default": "./export",
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
"required": ["dataset_id"],
|
|
661
|
+
},
|
|
662
|
+
),
|
|
663
|
+
]
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def get_deploy_tools() -> List[Tool]:
|
|
667
|
+
"""Deployment management tools"""
|
|
668
|
+
return [
|
|
669
|
+
Tool(
|
|
670
|
+
name="ate_deploy",
|
|
671
|
+
description="Deploy skills to a robot",
|
|
138
672
|
inputSchema={
|
|
139
673
|
"type": "object",
|
|
140
674
|
"properties": {
|
|
141
|
-
"
|
|
675
|
+
"robot_type": {
|
|
142
676
|
"type": "string",
|
|
143
|
-
"description": "
|
|
677
|
+
"description": "Robot type to deploy to",
|
|
144
678
|
},
|
|
145
|
-
"
|
|
679
|
+
"repo_id": {
|
|
146
680
|
"type": "string",
|
|
147
|
-
"description": "
|
|
148
|
-
},
|
|
149
|
-
"limit": {
|
|
150
|
-
"type": "number",
|
|
151
|
-
"description": "Maximum number of results",
|
|
152
|
-
"default": 20,
|
|
681
|
+
"description": "Repository ID (uses current repo if not specified)",
|
|
153
682
|
},
|
|
154
683
|
},
|
|
684
|
+
"required": ["robot_type"],
|
|
155
685
|
},
|
|
156
686
|
),
|
|
157
687
|
Tool(
|
|
158
|
-
name="
|
|
159
|
-
description="
|
|
688
|
+
name="ate_deploy_config",
|
|
689
|
+
description="Deploy skills using a deployment configuration file (supports hybrid edge/cloud)",
|
|
160
690
|
inputSchema={
|
|
161
691
|
"type": "object",
|
|
162
692
|
"properties": {
|
|
163
|
-
"
|
|
693
|
+
"config_path": {
|
|
164
694
|
"type": "string",
|
|
165
|
-
"description": "
|
|
695
|
+
"description": "Path to deploy.yaml configuration",
|
|
166
696
|
},
|
|
167
|
-
"
|
|
697
|
+
"target": {
|
|
168
698
|
"type": "string",
|
|
169
|
-
"description": "Target robot
|
|
699
|
+
"description": "Target fleet or robot",
|
|
170
700
|
},
|
|
171
|
-
"
|
|
172
|
-
"type": "
|
|
173
|
-
"description": "
|
|
701
|
+
"dry_run": {
|
|
702
|
+
"type": "boolean",
|
|
703
|
+
"description": "Show deployment plan without executing",
|
|
704
|
+
"default": False,
|
|
174
705
|
},
|
|
175
706
|
},
|
|
176
|
-
"required": ["
|
|
707
|
+
"required": ["config_path", "target"],
|
|
177
708
|
},
|
|
178
709
|
),
|
|
179
710
|
Tool(
|
|
180
|
-
name="
|
|
181
|
-
description="
|
|
711
|
+
name="ate_deploy_status",
|
|
712
|
+
description="Check deployment status for a fleet or robot",
|
|
182
713
|
inputSchema={
|
|
183
714
|
"type": "object",
|
|
184
715
|
"properties": {
|
|
185
|
-
"
|
|
716
|
+
"target": {
|
|
186
717
|
"type": "string",
|
|
187
|
-
"description": "
|
|
718
|
+
"description": "Target fleet or robot",
|
|
188
719
|
},
|
|
189
|
-
|
|
720
|
+
},
|
|
721
|
+
"required": ["target"],
|
|
722
|
+
},
|
|
723
|
+
),
|
|
724
|
+
]
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def get_test_tools() -> List[Tool]:
|
|
728
|
+
"""Testing and validation tools"""
|
|
729
|
+
return [
|
|
730
|
+
Tool(
|
|
731
|
+
name="ate_test",
|
|
732
|
+
description="Test skills in simulation (Gazebo, MuJoCo, PyBullet, Webots)",
|
|
733
|
+
inputSchema={
|
|
734
|
+
"type": "object",
|
|
735
|
+
"properties": {
|
|
736
|
+
"environment": {
|
|
190
737
|
"type": "string",
|
|
191
|
-
"
|
|
738
|
+
"enum": ["gazebo", "mujoco", "pybullet", "webots"],
|
|
739
|
+
"description": "Simulation environment",
|
|
740
|
+
"default": "pybullet",
|
|
192
741
|
},
|
|
193
|
-
"
|
|
742
|
+
"robot": {
|
|
194
743
|
"type": "string",
|
|
195
|
-
"description": "
|
|
744
|
+
"description": "Robot model to test with",
|
|
196
745
|
},
|
|
197
|
-
"
|
|
746
|
+
"local": {
|
|
198
747
|
"type": "boolean",
|
|
199
|
-
"description": "
|
|
748
|
+
"description": "Run simulation locally",
|
|
200
749
|
"default": False,
|
|
201
750
|
},
|
|
202
751
|
},
|
|
203
|
-
"required": ["source_robot_id", "target_robot_id", "repository_id"],
|
|
204
752
|
},
|
|
205
753
|
),
|
|
206
754
|
Tool(
|
|
207
|
-
name="
|
|
208
|
-
description="
|
|
755
|
+
name="ate_validate",
|
|
756
|
+
description="Run safety and compliance validation checks",
|
|
209
757
|
inputSchema={
|
|
210
758
|
"type": "object",
|
|
211
759
|
"properties": {
|
|
212
|
-
"
|
|
213
|
-
"type": "
|
|
214
|
-
"
|
|
760
|
+
"checks": {
|
|
761
|
+
"type": "array",
|
|
762
|
+
"items": {"type": "string"},
|
|
763
|
+
"description": "Safety checks to run (collision, speed, workspace, force, all)",
|
|
764
|
+
"default": ["all"],
|
|
765
|
+
},
|
|
766
|
+
"strict": {
|
|
767
|
+
"type": "boolean",
|
|
768
|
+
"description": "Use strict validation (fail on warnings)",
|
|
769
|
+
"default": False,
|
|
215
770
|
},
|
|
216
771
|
},
|
|
217
|
-
"required": ["repo_id"],
|
|
218
772
|
},
|
|
219
773
|
),
|
|
220
774
|
Tool(
|
|
221
|
-
name="
|
|
222
|
-
description="
|
|
775
|
+
name="ate_benchmark",
|
|
776
|
+
description="Run performance benchmarks on skills",
|
|
223
777
|
inputSchema={
|
|
224
778
|
"type": "object",
|
|
225
779
|
"properties": {
|
|
226
|
-
"
|
|
780
|
+
"type": {
|
|
227
781
|
"type": "string",
|
|
228
|
-
"
|
|
782
|
+
"enum": ["speed", "accuracy", "robustness", "efficiency", "all"],
|
|
783
|
+
"description": "Benchmark type",
|
|
784
|
+
"default": "all",
|
|
785
|
+
},
|
|
786
|
+
"trials": {
|
|
787
|
+
"type": "number",
|
|
788
|
+
"description": "Number of trials",
|
|
789
|
+
"default": 10,
|
|
790
|
+
},
|
|
791
|
+
"compare": {
|
|
792
|
+
"type": "string",
|
|
793
|
+
"description": "Compare with baseline repository ID",
|
|
229
794
|
},
|
|
230
795
|
},
|
|
231
|
-
"required": ["robot_id"],
|
|
232
796
|
},
|
|
233
797
|
),
|
|
234
798
|
]
|
|
235
799
|
|
|
236
800
|
|
|
801
|
+
@server.list_tools()
|
|
802
|
+
async def list_tools() -> List[Tool]:
|
|
803
|
+
"""List all available MCP tools"""
|
|
804
|
+
tools = []
|
|
805
|
+
tools.extend(get_repository_tools())
|
|
806
|
+
tools.extend(get_robot_tools())
|
|
807
|
+
tools.extend(get_compatibility_tools())
|
|
808
|
+
tools.extend(get_skill_tools())
|
|
809
|
+
tools.extend(get_parts_tools())
|
|
810
|
+
tools.extend(get_generate_tools())
|
|
811
|
+
tools.extend(get_workflow_tools())
|
|
812
|
+
tools.extend(get_team_tools())
|
|
813
|
+
tools.extend(get_data_tools())
|
|
814
|
+
tools.extend(get_deploy_tools())
|
|
815
|
+
tools.extend(get_test_tools())
|
|
816
|
+
return tools
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
# ============================================================================
|
|
820
|
+
# Tool Handlers
|
|
821
|
+
# ============================================================================
|
|
822
|
+
|
|
823
|
+
def capture_output(func, *args, **kwargs):
|
|
824
|
+
"""Capture printed output from a function"""
|
|
825
|
+
import io
|
|
826
|
+
import contextlib
|
|
827
|
+
|
|
828
|
+
f = io.StringIO()
|
|
829
|
+
with contextlib.redirect_stdout(f):
|
|
830
|
+
try:
|
|
831
|
+
result = func(*args, **kwargs)
|
|
832
|
+
except SystemExit:
|
|
833
|
+
pass # CLI functions may call sys.exit
|
|
834
|
+
return f.getvalue()
|
|
835
|
+
|
|
836
|
+
|
|
237
837
|
@server.call_tool()
|
|
238
838
|
async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|
239
839
|
"""Handle tool calls"""
|
|
240
840
|
try:
|
|
841
|
+
# Repository tools
|
|
241
842
|
if name == "ate_init":
|
|
242
843
|
result = client.init(
|
|
243
844
|
arguments["name"],
|
|
@@ -252,18 +853,14 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|
|
252
853
|
]
|
|
253
854
|
|
|
254
855
|
elif name == "ate_clone":
|
|
255
|
-
|
|
256
|
-
|
|
856
|
+
output = capture_output(
|
|
857
|
+
client.clone,
|
|
858
|
+
arguments["repo_id"],
|
|
859
|
+
arguments.get("target_dir")
|
|
257
860
|
)
|
|
258
|
-
return [
|
|
259
|
-
TextContent(
|
|
260
|
-
type="text",
|
|
261
|
-
text=f"Repository cloned successfully to {arguments.get('target_dir', 'current directory')}",
|
|
262
|
-
)
|
|
263
|
-
]
|
|
861
|
+
return [TextContent(type="text", text=output or f"Repository cloned successfully")]
|
|
264
862
|
|
|
265
863
|
elif name == "ate_list_repositories":
|
|
266
|
-
# Build query params
|
|
267
864
|
params = {}
|
|
268
865
|
if arguments.get("search"):
|
|
269
866
|
params["search"] = arguments["search"]
|
|
@@ -271,20 +868,30 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|
|
271
868
|
params["robotModel"] = arguments["robot_model"]
|
|
272
869
|
params["limit"] = arguments.get("limit", 20)
|
|
273
870
|
|
|
274
|
-
# Make API request
|
|
275
871
|
response = client._request("GET", "/repositories", params=params)
|
|
276
872
|
repos = response.get("repositories", [])
|
|
277
873
|
|
|
278
874
|
result_text = f"Found {len(repos)} repositories:\n\n"
|
|
279
|
-
for repo in repos[:10]:
|
|
875
|
+
for repo in repos[:10]:
|
|
280
876
|
result_text += f"- {repo['name']} (ID: {repo['id']})\n"
|
|
281
877
|
if repo.get("description"):
|
|
282
878
|
result_text += f" {repo['description'][:100]}...\n"
|
|
283
879
|
|
|
284
880
|
return [TextContent(type="text", text=result_text)]
|
|
285
881
|
|
|
882
|
+
elif name == "ate_get_repository":
|
|
883
|
+
response = client._request("GET", f"/repositories/{arguments['repo_id']}")
|
|
884
|
+
repo = response.get("repository", {})
|
|
885
|
+
|
|
886
|
+
result_text = f"Repository: {repo.get('name', 'Unknown')}\n"
|
|
887
|
+
result_text += f"ID: {repo.get('id', 'Unknown')}\n"
|
|
888
|
+
result_text += f"Description: {repo.get('description', 'No description')}\n"
|
|
889
|
+
result_text += f"Visibility: {repo.get('visibility', 'unknown')}\n"
|
|
890
|
+
|
|
891
|
+
return [TextContent(type="text", text=result_text)]
|
|
892
|
+
|
|
893
|
+
# Robot tools
|
|
286
894
|
elif name == "ate_list_robots":
|
|
287
|
-
# Build query params
|
|
288
895
|
params = {}
|
|
289
896
|
if arguments.get("search"):
|
|
290
897
|
params["search"] = arguments["search"]
|
|
@@ -292,80 +899,257 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|
|
292
899
|
params["category"] = arguments["category"]
|
|
293
900
|
params["limit"] = arguments.get("limit", 20)
|
|
294
901
|
|
|
295
|
-
# Make API request
|
|
296
902
|
response = client._request("GET", "/robots/profiles", params=params)
|
|
297
903
|
robots = response.get("profiles", [])
|
|
298
904
|
|
|
299
905
|
result_text = f"Found {len(robots)} robot profiles:\n\n"
|
|
300
|
-
for robot in robots[:10]:
|
|
906
|
+
for robot in robots[:10]:
|
|
301
907
|
result_text += f"- {robot['modelName']} by {robot['manufacturer']} (ID: {robot['id']})\n"
|
|
302
908
|
if robot.get("description"):
|
|
303
909
|
result_text += f" {robot['description'][:100]}...\n"
|
|
304
910
|
|
|
305
911
|
return [TextContent(type="text", text=result_text)]
|
|
306
912
|
|
|
307
|
-
elif name == "
|
|
308
|
-
response = client._request(
|
|
309
|
-
|
|
310
|
-
"/skills/compatibility",
|
|
311
|
-
json={
|
|
312
|
-
"sourceRobotId": arguments["source_robot_id"],
|
|
313
|
-
"targetRobotId": arguments["target_robot_id"],
|
|
314
|
-
"repositoryId": arguments["repository_id"],
|
|
315
|
-
},
|
|
316
|
-
)
|
|
317
|
-
compatibility = response.get("compatibility", {})
|
|
318
|
-
score = compatibility.get("overallScore", 0) * 100
|
|
913
|
+
elif name == "ate_get_robot":
|
|
914
|
+
response = client._request("GET", f"/robots/profiles/{arguments['robot_id']}")
|
|
915
|
+
robot = response.get("profile", {})
|
|
319
916
|
|
|
320
|
-
result_text = f"
|
|
321
|
-
result_text += f"
|
|
322
|
-
result_text += f"
|
|
917
|
+
result_text = f"Robot: {robot.get('modelName', 'Unknown')}\n"
|
|
918
|
+
result_text += f"Manufacturer: {robot.get('manufacturer', 'Unknown')}\n"
|
|
919
|
+
result_text += f"Category: {robot.get('category', 'Unknown')}\n"
|
|
920
|
+
result_text += f"Description: {robot.get('description', 'No description')}\n"
|
|
323
921
|
|
|
324
922
|
return [TextContent(type="text", text=result_text)]
|
|
325
923
|
|
|
924
|
+
# Compatibility tools
|
|
925
|
+
elif name == "ate_check_transfer":
|
|
926
|
+
output = capture_output(
|
|
927
|
+
client.check_transfer,
|
|
928
|
+
arguments.get("skill_id"),
|
|
929
|
+
arguments["source_robot"],
|
|
930
|
+
arguments["target_robot"],
|
|
931
|
+
arguments.get("min_score", 0.0)
|
|
932
|
+
)
|
|
933
|
+
return [TextContent(type="text", text=output)]
|
|
934
|
+
|
|
326
935
|
elif name == "ate_adapt":
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
"
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"repositoryId": arguments["repository_id"],
|
|
334
|
-
},
|
|
936
|
+
output = capture_output(
|
|
937
|
+
client.adapt,
|
|
938
|
+
arguments["source_robot"],
|
|
939
|
+
arguments["target_robot"],
|
|
940
|
+
arguments.get("repo_id"),
|
|
941
|
+
arguments.get("analyze_only", True)
|
|
335
942
|
)
|
|
336
|
-
|
|
337
|
-
compatibility = response.get("compatibility", {})
|
|
943
|
+
return [TextContent(type="text", text=output)]
|
|
338
944
|
|
|
339
|
-
|
|
340
|
-
|
|
945
|
+
# Skill tools
|
|
946
|
+
elif name == "ate_pull":
|
|
947
|
+
output = capture_output(
|
|
948
|
+
client.pull,
|
|
949
|
+
arguments["skill_id"],
|
|
950
|
+
arguments.get("robot"),
|
|
951
|
+
arguments.get("format", "json"),
|
|
952
|
+
arguments.get("output", "./data")
|
|
953
|
+
)
|
|
954
|
+
return [TextContent(type="text", text=output)]
|
|
341
955
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
956
|
+
elif name == "ate_upload":
|
|
957
|
+
output = capture_output(
|
|
958
|
+
client.upload,
|
|
959
|
+
arguments["path"],
|
|
960
|
+
arguments["robot"],
|
|
961
|
+
arguments["task"],
|
|
962
|
+
arguments.get("project")
|
|
963
|
+
)
|
|
964
|
+
return [TextContent(type="text", text=output)]
|
|
345
965
|
|
|
346
|
-
|
|
966
|
+
# Parts tools
|
|
967
|
+
elif name == "ate_parts_list":
|
|
968
|
+
output = capture_output(
|
|
969
|
+
client.parts_list,
|
|
970
|
+
arguments.get("category"),
|
|
971
|
+
arguments.get("manufacturer"),
|
|
972
|
+
arguments.get("search")
|
|
973
|
+
)
|
|
974
|
+
return [TextContent(type="text", text=output)]
|
|
347
975
|
|
|
348
|
-
elif name == "
|
|
349
|
-
|
|
350
|
-
|
|
976
|
+
elif name == "ate_parts_check":
|
|
977
|
+
output = capture_output(
|
|
978
|
+
client.parts_check,
|
|
979
|
+
arguments["skill_id"]
|
|
980
|
+
)
|
|
981
|
+
return [TextContent(type="text", text=output)]
|
|
351
982
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
983
|
+
elif name == "ate_parts_require":
|
|
984
|
+
output = capture_output(
|
|
985
|
+
client.parts_require,
|
|
986
|
+
arguments["part_id"],
|
|
987
|
+
arguments["skill_id"],
|
|
988
|
+
arguments.get("version", "1.0.0"),
|
|
989
|
+
arguments.get("required", False)
|
|
990
|
+
)
|
|
991
|
+
return [TextContent(type="text", text=output)]
|
|
356
992
|
|
|
357
|
-
|
|
993
|
+
elif name == "ate_deps_audit":
|
|
994
|
+
output = capture_output(
|
|
995
|
+
client.deps_audit,
|
|
996
|
+
arguments.get("skill_id")
|
|
997
|
+
)
|
|
998
|
+
return [TextContent(type="text", text=output)]
|
|
358
999
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
1000
|
+
# Generate tools
|
|
1001
|
+
elif name == "ate_generate":
|
|
1002
|
+
output = capture_output(
|
|
1003
|
+
client.generate,
|
|
1004
|
+
arguments["description"],
|
|
1005
|
+
arguments.get("robot", "ur5"),
|
|
1006
|
+
arguments.get("output", "./new-skill")
|
|
1007
|
+
)
|
|
1008
|
+
return [TextContent(type="text", text=output)]
|
|
362
1009
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
1010
|
+
# Workflow tools
|
|
1011
|
+
elif name == "ate_workflow_validate":
|
|
1012
|
+
output = capture_output(
|
|
1013
|
+
client.workflow_validate,
|
|
1014
|
+
arguments["path"]
|
|
1015
|
+
)
|
|
1016
|
+
return [TextContent(type="text", text=output)]
|
|
367
1017
|
|
|
368
|
-
|
|
1018
|
+
elif name == "ate_workflow_run":
|
|
1019
|
+
output = capture_output(
|
|
1020
|
+
client.workflow_run,
|
|
1021
|
+
arguments["path"],
|
|
1022
|
+
arguments.get("sim", True),
|
|
1023
|
+
arguments.get("dry_run", False)
|
|
1024
|
+
)
|
|
1025
|
+
return [TextContent(type="text", text=output)]
|
|
1026
|
+
|
|
1027
|
+
elif name == "ate_workflow_export":
|
|
1028
|
+
output = capture_output(
|
|
1029
|
+
client.workflow_export,
|
|
1030
|
+
arguments["path"],
|
|
1031
|
+
arguments.get("format", "ros2"),
|
|
1032
|
+
arguments.get("output")
|
|
1033
|
+
)
|
|
1034
|
+
return [TextContent(type="text", text=output)]
|
|
1035
|
+
|
|
1036
|
+
# Team tools
|
|
1037
|
+
elif name == "ate_team_create":
|
|
1038
|
+
output = capture_output(
|
|
1039
|
+
client.team_create,
|
|
1040
|
+
arguments["name"],
|
|
1041
|
+
arguments.get("description")
|
|
1042
|
+
)
|
|
1043
|
+
return [TextContent(type="text", text=output)]
|
|
1044
|
+
|
|
1045
|
+
elif name == "ate_team_list":
|
|
1046
|
+
output = capture_output(client.team_list)
|
|
1047
|
+
return [TextContent(type="text", text=output)]
|
|
1048
|
+
|
|
1049
|
+
elif name == "ate_team_invite":
|
|
1050
|
+
output = capture_output(
|
|
1051
|
+
client.team_invite,
|
|
1052
|
+
arguments["email"],
|
|
1053
|
+
arguments["team"],
|
|
1054
|
+
arguments.get("role", "member")
|
|
1055
|
+
)
|
|
1056
|
+
return [TextContent(type="text", text=output)]
|
|
1057
|
+
|
|
1058
|
+
elif name == "ate_team_share":
|
|
1059
|
+
output = capture_output(
|
|
1060
|
+
client.team_share,
|
|
1061
|
+
arguments["skill_id"],
|
|
1062
|
+
arguments["team"]
|
|
1063
|
+
)
|
|
1064
|
+
return [TextContent(type="text", text=output)]
|
|
1065
|
+
|
|
1066
|
+
# Data tools
|
|
1067
|
+
elif name == "ate_data_upload":
|
|
1068
|
+
output = capture_output(
|
|
1069
|
+
client.data_upload,
|
|
1070
|
+
arguments["path"],
|
|
1071
|
+
arguments["skill"],
|
|
1072
|
+
arguments.get("stage", "raw")
|
|
1073
|
+
)
|
|
1074
|
+
return [TextContent(type="text", text=output)]
|
|
1075
|
+
|
|
1076
|
+
elif name == "ate_data_list":
|
|
1077
|
+
output = capture_output(
|
|
1078
|
+
client.data_list,
|
|
1079
|
+
arguments.get("skill"),
|
|
1080
|
+
arguments.get("stage")
|
|
1081
|
+
)
|
|
1082
|
+
return [TextContent(type="text", text=output)]
|
|
1083
|
+
|
|
1084
|
+
elif name == "ate_data_promote":
|
|
1085
|
+
output = capture_output(
|
|
1086
|
+
client.data_promote,
|
|
1087
|
+
arguments["dataset_id"],
|
|
1088
|
+
arguments["to_stage"]
|
|
1089
|
+
)
|
|
1090
|
+
return [TextContent(type="text", text=output)]
|
|
1091
|
+
|
|
1092
|
+
elif name == "ate_data_export":
|
|
1093
|
+
output = capture_output(
|
|
1094
|
+
client.data_export,
|
|
1095
|
+
arguments["dataset_id"],
|
|
1096
|
+
arguments.get("format", "rlds"),
|
|
1097
|
+
arguments.get("output", "./export")
|
|
1098
|
+
)
|
|
1099
|
+
return [TextContent(type="text", text=output)]
|
|
1100
|
+
|
|
1101
|
+
# Deploy tools
|
|
1102
|
+
elif name == "ate_deploy":
|
|
1103
|
+
output = capture_output(
|
|
1104
|
+
client.deploy,
|
|
1105
|
+
arguments["robot_type"],
|
|
1106
|
+
arguments.get("repo_id")
|
|
1107
|
+
)
|
|
1108
|
+
return [TextContent(type="text", text=output)]
|
|
1109
|
+
|
|
1110
|
+
elif name == "ate_deploy_config":
|
|
1111
|
+
output = capture_output(
|
|
1112
|
+
client.deploy_config,
|
|
1113
|
+
arguments["config_path"],
|
|
1114
|
+
arguments["target"],
|
|
1115
|
+
arguments.get("dry_run", False)
|
|
1116
|
+
)
|
|
1117
|
+
return [TextContent(type="text", text=output)]
|
|
1118
|
+
|
|
1119
|
+
elif name == "ate_deploy_status":
|
|
1120
|
+
output = capture_output(
|
|
1121
|
+
client.deploy_status,
|
|
1122
|
+
arguments["target"]
|
|
1123
|
+
)
|
|
1124
|
+
return [TextContent(type="text", text=output)]
|
|
1125
|
+
|
|
1126
|
+
# Test tools
|
|
1127
|
+
elif name == "ate_test":
|
|
1128
|
+
output = capture_output(
|
|
1129
|
+
client.test,
|
|
1130
|
+
arguments.get("environment", "pybullet"),
|
|
1131
|
+
arguments.get("robot"),
|
|
1132
|
+
arguments.get("local", False)
|
|
1133
|
+
)
|
|
1134
|
+
return [TextContent(type="text", text=output)]
|
|
1135
|
+
|
|
1136
|
+
elif name == "ate_validate":
|
|
1137
|
+
output = capture_output(
|
|
1138
|
+
client.validate,
|
|
1139
|
+
arguments.get("checks", ["all"]),
|
|
1140
|
+
arguments.get("strict", False),
|
|
1141
|
+
None # files
|
|
1142
|
+
)
|
|
1143
|
+
return [TextContent(type="text", text=output)]
|
|
1144
|
+
|
|
1145
|
+
elif name == "ate_benchmark":
|
|
1146
|
+
output = capture_output(
|
|
1147
|
+
client.benchmark,
|
|
1148
|
+
arguments.get("type", "all"),
|
|
1149
|
+
arguments.get("trials", 10),
|
|
1150
|
+
arguments.get("compare")
|
|
1151
|
+
)
|
|
1152
|
+
return [TextContent(type="text", text=output)]
|
|
369
1153
|
|
|
370
1154
|
else:
|
|
371
1155
|
return [
|
|
@@ -384,6 +1168,10 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|
|
384
1168
|
]
|
|
385
1169
|
|
|
386
1170
|
|
|
1171
|
+
# ============================================================================
|
|
1172
|
+
# Resources
|
|
1173
|
+
# ============================================================================
|
|
1174
|
+
|
|
387
1175
|
@server.list_resources()
|
|
388
1176
|
async def list_resources() -> List[Resource]:
|
|
389
1177
|
"""List available resources"""
|
|
@@ -400,6 +1188,30 @@ async def list_resources() -> List[Resource]:
|
|
|
400
1188
|
description="Access robot profile details",
|
|
401
1189
|
mimeType="application/json",
|
|
402
1190
|
),
|
|
1191
|
+
Resource(
|
|
1192
|
+
uri="skill://*",
|
|
1193
|
+
name="Skill",
|
|
1194
|
+
description="Access skill/artifact details",
|
|
1195
|
+
mimeType="application/json",
|
|
1196
|
+
),
|
|
1197
|
+
Resource(
|
|
1198
|
+
uri="part://*",
|
|
1199
|
+
name="Hardware Part",
|
|
1200
|
+
description="Access hardware part details",
|
|
1201
|
+
mimeType="application/json",
|
|
1202
|
+
),
|
|
1203
|
+
Resource(
|
|
1204
|
+
uri="workflow://*",
|
|
1205
|
+
name="Workflow",
|
|
1206
|
+
description="Access workflow definition",
|
|
1207
|
+
mimeType="application/yaml",
|
|
1208
|
+
),
|
|
1209
|
+
Resource(
|
|
1210
|
+
uri="team://*",
|
|
1211
|
+
name="Team",
|
|
1212
|
+
description="Access team details",
|
|
1213
|
+
mimeType="application/json",
|
|
1214
|
+
),
|
|
403
1215
|
]
|
|
404
1216
|
|
|
405
1217
|
|
|
@@ -414,17 +1226,33 @@ async def read_resource(uri: str) -> str:
|
|
|
414
1226
|
robot_id = uri.replace("robot://", "")
|
|
415
1227
|
response = client._request("GET", f"/robots/profiles/{robot_id}")
|
|
416
1228
|
return json.dumps(response.get("profile", {}), indent=2)
|
|
1229
|
+
elif uri.startswith("skill://"):
|
|
1230
|
+
skill_id = uri.replace("skill://", "")
|
|
1231
|
+
response = client._request("GET", f"/skills/{skill_id}")
|
|
1232
|
+
return json.dumps(response.get("skill", {}), indent=2)
|
|
1233
|
+
elif uri.startswith("part://"):
|
|
1234
|
+
part_id = uri.replace("part://", "")
|
|
1235
|
+
response = client._request("GET", f"/parts/{part_id}")
|
|
1236
|
+
return json.dumps(response.get("part", {}), indent=2)
|
|
1237
|
+
elif uri.startswith("team://"):
|
|
1238
|
+
team_slug = uri.replace("team://", "")
|
|
1239
|
+
response = client._request("GET", f"/teams/{team_slug}")
|
|
1240
|
+
return json.dumps(response.get("team", {}), indent=2)
|
|
417
1241
|
else:
|
|
418
1242
|
raise ValueError(f"Unknown resource URI: {uri}")
|
|
419
1243
|
|
|
420
1244
|
|
|
1245
|
+
# ============================================================================
|
|
1246
|
+
# Prompts
|
|
1247
|
+
# ============================================================================
|
|
1248
|
+
|
|
421
1249
|
@server.list_prompts()
|
|
422
1250
|
async def list_prompts() -> List[Prompt]:
|
|
423
1251
|
"""List available prompts"""
|
|
424
1252
|
return [
|
|
425
1253
|
Prompt(
|
|
426
1254
|
name="create_skill",
|
|
427
|
-
description="Guided workflow for creating a new robot skill
|
|
1255
|
+
description="Guided workflow for creating a new robot skill from scratch",
|
|
428
1256
|
arguments=[
|
|
429
1257
|
PromptArgument(
|
|
430
1258
|
name="robot_model",
|
|
@@ -433,14 +1261,14 @@ async def list_prompts() -> List[Prompt]:
|
|
|
433
1261
|
),
|
|
434
1262
|
PromptArgument(
|
|
435
1263
|
name="task_description",
|
|
436
|
-
description="
|
|
1264
|
+
description="Natural language description of the skill/task",
|
|
437
1265
|
required=True,
|
|
438
1266
|
),
|
|
439
1267
|
],
|
|
440
1268
|
),
|
|
441
1269
|
Prompt(
|
|
442
1270
|
name="adapt_skill",
|
|
443
|
-
description="Guided workflow for adapting a skill between robots",
|
|
1271
|
+
description="Guided workflow for adapting a skill between different robots",
|
|
444
1272
|
arguments=[
|
|
445
1273
|
PromptArgument(
|
|
446
1274
|
name="source_robot",
|
|
@@ -459,6 +1287,59 @@ async def list_prompts() -> List[Prompt]:
|
|
|
459
1287
|
),
|
|
460
1288
|
],
|
|
461
1289
|
),
|
|
1290
|
+
Prompt(
|
|
1291
|
+
name="setup_workflow",
|
|
1292
|
+
description="Create a multi-skill workflow/pipeline",
|
|
1293
|
+
arguments=[
|
|
1294
|
+
PromptArgument(
|
|
1295
|
+
name="task_description",
|
|
1296
|
+
description="Description of the overall task",
|
|
1297
|
+
required=True,
|
|
1298
|
+
),
|
|
1299
|
+
PromptArgument(
|
|
1300
|
+
name="robot",
|
|
1301
|
+
description="Target robot model",
|
|
1302
|
+
required=True,
|
|
1303
|
+
),
|
|
1304
|
+
],
|
|
1305
|
+
),
|
|
1306
|
+
Prompt(
|
|
1307
|
+
name="deploy_skill",
|
|
1308
|
+
description="Deploy a skill to production robots",
|
|
1309
|
+
arguments=[
|
|
1310
|
+
PromptArgument(
|
|
1311
|
+
name="skill_id",
|
|
1312
|
+
description="Skill ID to deploy",
|
|
1313
|
+
required=True,
|
|
1314
|
+
),
|
|
1315
|
+
PromptArgument(
|
|
1316
|
+
name="target",
|
|
1317
|
+
description="Target fleet or robot",
|
|
1318
|
+
required=True,
|
|
1319
|
+
),
|
|
1320
|
+
],
|
|
1321
|
+
),
|
|
1322
|
+
Prompt(
|
|
1323
|
+
name="debug_compatibility",
|
|
1324
|
+
description="Debug why a skill isn't transferring well between robots",
|
|
1325
|
+
arguments=[
|
|
1326
|
+
PromptArgument(
|
|
1327
|
+
name="source_robot",
|
|
1328
|
+
description="Source robot model",
|
|
1329
|
+
required=True,
|
|
1330
|
+
),
|
|
1331
|
+
PromptArgument(
|
|
1332
|
+
name="target_robot",
|
|
1333
|
+
description="Target robot model",
|
|
1334
|
+
required=True,
|
|
1335
|
+
),
|
|
1336
|
+
PromptArgument(
|
|
1337
|
+
name="skill_id",
|
|
1338
|
+
description="Skill ID having issues",
|
|
1339
|
+
required=True,
|
|
1340
|
+
),
|
|
1341
|
+
],
|
|
1342
|
+
),
|
|
462
1343
|
]
|
|
463
1344
|
|
|
464
1345
|
|
|
@@ -469,32 +1350,195 @@ async def get_prompt(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|
|
469
1350
|
return [
|
|
470
1351
|
TextContent(
|
|
471
1352
|
type="text",
|
|
472
|
-
text=f"""Create a
|
|
1353
|
+
text=f"""# Create a Robot Skill for {arguments.get('robot_model', 'your robot')}
|
|
1354
|
+
|
|
1355
|
+
## Task: {arguments.get('task_description', 'Not specified')}
|
|
1356
|
+
|
|
1357
|
+
### Steps:
|
|
473
1358
|
|
|
474
|
-
1.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
4. Push to FoodforThought: Use ate_push
|
|
1359
|
+
1. **Generate Scaffolding**
|
|
1360
|
+
Use `ate_generate` to create skill files from your task description:
|
|
1361
|
+
- This creates skill.yaml, main.py, test_skill.py, and README.md
|
|
478
1362
|
|
|
479
|
-
|
|
1363
|
+
2. **Review Generated Files**
|
|
1364
|
+
- Check skill.yaml for correct parameters
|
|
1365
|
+
- Implement the TODO sections in main.py
|
|
1366
|
+
|
|
1367
|
+
3. **Add Part Dependencies**
|
|
1368
|
+
Use `ate_parts_list` to find required hardware, then `ate_parts_require` to add dependencies
|
|
1369
|
+
|
|
1370
|
+
4. **Test in Simulation**
|
|
1371
|
+
Use `ate_test` with environment="pybullet" to validate the skill
|
|
1372
|
+
|
|
1373
|
+
5. **Run Safety Validation**
|
|
1374
|
+
Use `ate_validate` with checks=["collision", "speed", "workspace", "force"]
|
|
1375
|
+
|
|
1376
|
+
6. **Upload Demonstrations**
|
|
1377
|
+
Use `ate_upload` to submit demo videos for community labeling
|
|
1378
|
+
|
|
1379
|
+
7. **Check Transfer Compatibility**
|
|
1380
|
+
Use `ate_check_transfer` to see which other robots can use this skill
|
|
480
1381
|
""",
|
|
481
1382
|
)
|
|
482
1383
|
]
|
|
1384
|
+
|
|
483
1385
|
elif name == "adapt_skill":
|
|
484
1386
|
return [
|
|
485
1387
|
TextContent(
|
|
486
1388
|
type="text",
|
|
487
|
-
text=f"""Adapt
|
|
1389
|
+
text=f"""# Adapt Skill from {arguments.get('source_robot')} to {arguments.get('target_robot')}
|
|
1390
|
+
|
|
1391
|
+
## Repository: {arguments.get('repository_id')}
|
|
1392
|
+
|
|
1393
|
+
### Steps:
|
|
1394
|
+
|
|
1395
|
+
1. **Check Compatibility**
|
|
1396
|
+
Use `ate_check_transfer` to get the compatibility score and adaptation type
|
|
1397
|
+
|
|
1398
|
+
2. **Generate Adaptation Plan**
|
|
1399
|
+
Use `ate_adapt` to see what changes are needed:
|
|
1400
|
+
- Kinematic adaptations
|
|
1401
|
+
- Sensor mappings
|
|
1402
|
+
- Code modifications
|
|
1403
|
+
|
|
1404
|
+
3. **Review Requirements**
|
|
1405
|
+
- Check if new parts are needed with `ate_parts_check`
|
|
1406
|
+
- Verify hardware compatibility
|
|
1407
|
+
|
|
1408
|
+
4. **Apply Adaptations**
|
|
1409
|
+
Based on the adaptation type:
|
|
1410
|
+
- **Direct**: No changes needed
|
|
1411
|
+
- **Parametric**: Adjust configuration values
|
|
1412
|
+
- **Retrain**: Collect new demonstrations
|
|
1413
|
+
- **Manual**: Significant code changes required
|
|
1414
|
+
|
|
1415
|
+
5. **Test Adapted Skill**
|
|
1416
|
+
Use `ate_test` with robot="{arguments.get('target_robot')}"
|
|
1417
|
+
|
|
1418
|
+
6. **Validate Safety**
|
|
1419
|
+
Use `ate_validate` with strict=true for production deployment
|
|
1420
|
+
""",
|
|
1421
|
+
)
|
|
1422
|
+
]
|
|
1423
|
+
|
|
1424
|
+
elif name == "setup_workflow":
|
|
1425
|
+
return [
|
|
1426
|
+
TextContent(
|
|
1427
|
+
type="text",
|
|
1428
|
+
text=f"""# Create Multi-Skill Workflow
|
|
1429
|
+
|
|
1430
|
+
## Task: {arguments.get('task_description', 'Not specified')}
|
|
1431
|
+
## Robot: {arguments.get('robot', 'Not specified')}
|
|
1432
|
+
|
|
1433
|
+
### Steps:
|
|
1434
|
+
|
|
1435
|
+
1. **Define Workflow Steps**
|
|
1436
|
+
Create a workflow.yaml file with your skill pipeline:
|
|
1437
|
+
```yaml
|
|
1438
|
+
name: My Workflow
|
|
1439
|
+
version: 1.0.0
|
|
1440
|
+
robot:
|
|
1441
|
+
model: {arguments.get('robot', 'ur5')}
|
|
1442
|
+
steps:
|
|
1443
|
+
- id: step1
|
|
1444
|
+
skill: perception/detect-object
|
|
1445
|
+
- id: step2
|
|
1446
|
+
skill: manipulation/pick
|
|
1447
|
+
depends_on: [step1]
|
|
1448
|
+
```
|
|
1449
|
+
|
|
1450
|
+
2. **Validate Workflow**
|
|
1451
|
+
Use `ate_workflow_validate` to check for errors
|
|
1452
|
+
|
|
1453
|
+
3. **Test in Simulation**
|
|
1454
|
+
Use `ate_workflow_run` with sim=true
|
|
1455
|
+
|
|
1456
|
+
4. **Dry Run**
|
|
1457
|
+
Use `ate_workflow_run` with dry_run=true to see execution plan
|
|
1458
|
+
|
|
1459
|
+
5. **Export for Production**
|
|
1460
|
+
Use `ate_workflow_export` with format="ros2" for ROS2 launch file
|
|
1461
|
+
""",
|
|
1462
|
+
)
|
|
1463
|
+
]
|
|
1464
|
+
|
|
1465
|
+
elif name == "deploy_skill":
|
|
1466
|
+
return [
|
|
1467
|
+
TextContent(
|
|
1468
|
+
type="text",
|
|
1469
|
+
text=f"""# Deploy Skill to Production
|
|
1470
|
+
|
|
1471
|
+
## Skill: {arguments.get('skill_id')}
|
|
1472
|
+
## Target: {arguments.get('target')}
|
|
1473
|
+
|
|
1474
|
+
### Pre-Deployment Checklist:
|
|
1475
|
+
|
|
1476
|
+
1. **Audit Dependencies**
|
|
1477
|
+
Use `ate_deps_audit` to verify all parts are available
|
|
1478
|
+
|
|
1479
|
+
2. **Run Validation**
|
|
1480
|
+
Use `ate_validate` with strict=true
|
|
1481
|
+
|
|
1482
|
+
3. **Benchmark Performance**
|
|
1483
|
+
Use `ate_benchmark` to ensure acceptable performance
|
|
1484
|
+
|
|
1485
|
+
4. **Check Deployment Config**
|
|
1486
|
+
Create deploy.yaml for hybrid edge/cloud deployment if needed
|
|
1487
|
+
|
|
1488
|
+
### Deployment Steps:
|
|
1489
|
+
|
|
1490
|
+
1. **Dry Run**
|
|
1491
|
+
Use `ate_deploy_config` with dry_run=true to preview
|
|
1492
|
+
|
|
1493
|
+
2. **Deploy**
|
|
1494
|
+
Use `ate_deploy` or `ate_deploy_config` to push to target
|
|
1495
|
+
|
|
1496
|
+
3. **Monitor Status**
|
|
1497
|
+
Use `ate_deploy_status` to check deployment health
|
|
1498
|
+
""",
|
|
1499
|
+
)
|
|
1500
|
+
]
|
|
1501
|
+
|
|
1502
|
+
elif name == "debug_compatibility":
|
|
1503
|
+
return [
|
|
1504
|
+
TextContent(
|
|
1505
|
+
type="text",
|
|
1506
|
+
text=f"""# Debug Skill Transfer Compatibility
|
|
488
1507
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
4. Test the adapted skill
|
|
1508
|
+
## Source: {arguments.get('source_robot')}
|
|
1509
|
+
## Target: {arguments.get('target_robot')}
|
|
1510
|
+
## Skill: {arguments.get('skill_id')}
|
|
493
1511
|
|
|
494
|
-
|
|
1512
|
+
### Diagnostic Steps:
|
|
1513
|
+
|
|
1514
|
+
1. **Get Compatibility Score**
|
|
1515
|
+
Use `ate_check_transfer` to see overall compatibility
|
|
1516
|
+
|
|
1517
|
+
2. **Check Score Breakdown**
|
|
1518
|
+
Look at individual scores:
|
|
1519
|
+
- Kinematic score: Joint configurations, workspace overlap
|
|
1520
|
+
- Sensor score: Camera, force/torque sensor compatibility
|
|
1521
|
+
- Compute score: Processing power requirements
|
|
1522
|
+
|
|
1523
|
+
3. **Review Part Requirements**
|
|
1524
|
+
Use `ate_parts_check` to see required hardware for the skill
|
|
1525
|
+
|
|
1526
|
+
4. **Compare Robot Profiles**
|
|
1527
|
+
Use `ate_get_robot` for both source and target to compare specs
|
|
1528
|
+
|
|
1529
|
+
5. **Generate Adaptation Plan**
|
|
1530
|
+
Use `ate_adapt` to get specific recommendations
|
|
1531
|
+
|
|
1532
|
+
### Common Issues:
|
|
1533
|
+
|
|
1534
|
+
- **Low Kinematic Score**: Check joint limits, reach, payload
|
|
1535
|
+
- **Low Sensor Score**: Missing cameras or sensors
|
|
1536
|
+
- **Low Compute Score**: Target robot has less processing power
|
|
1537
|
+
- **Impossible Transfer**: Fundamentally incompatible hardware
|
|
495
1538
|
""",
|
|
496
1539
|
)
|
|
497
1540
|
]
|
|
1541
|
+
|
|
498
1542
|
else:
|
|
499
1543
|
return [TextContent(type="text", text=f"Unknown prompt: {name}")]
|
|
500
1544
|
|
|
@@ -502,7 +1546,6 @@ Repository ID: {arguments.get('repository_id')}
|
|
|
502
1546
|
async def main():
|
|
503
1547
|
"""Main entry point for MCP server"""
|
|
504
1548
|
# Run the server using stdio transport
|
|
505
|
-
# stdio_server() returns (stdin, stdout) streams
|
|
506
1549
|
stdin, stdout = stdio_server()
|
|
507
1550
|
await server.run(
|
|
508
1551
|
stdin, stdout, server.create_initialization_options()
|
|
@@ -511,4 +1554,3 @@ async def main():
|
|
|
511
1554
|
|
|
512
1555
|
if __name__ == "__main__":
|
|
513
1556
|
asyncio.run(main())
|
|
514
|
-
|