foodforthought-cli 0.2.1__py3-none-any.whl → 0.2.4__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.
Files changed (46) hide show
  1. ate/__init__.py +1 -1
  2. ate/bridge_server.py +622 -0
  3. ate/cli.py +2625 -242
  4. ate/compatibility.py +580 -0
  5. ate/generators/__init__.py +19 -0
  6. ate/generators/docker_generator.py +461 -0
  7. ate/generators/hardware_config.py +469 -0
  8. ate/generators/ros2_generator.py +617 -0
  9. ate/generators/skill_generator.py +783 -0
  10. ate/marketplace.py +524 -0
  11. ate/mcp_server.py +1341 -107
  12. ate/primitives.py +1016 -0
  13. ate/robot_setup.py +2222 -0
  14. ate/skill_schema.py +537 -0
  15. ate/telemetry/__init__.py +33 -0
  16. ate/telemetry/cli.py +455 -0
  17. ate/telemetry/collector.py +444 -0
  18. ate/telemetry/context.py +318 -0
  19. ate/telemetry/fleet_agent.py +419 -0
  20. ate/telemetry/formats/__init__.py +18 -0
  21. ate/telemetry/formats/hdf5_serializer.py +503 -0
  22. ate/telemetry/formats/mcap_serializer.py +457 -0
  23. ate/telemetry/types.py +334 -0
  24. foodforthought_cli-0.2.4.dist-info/METADATA +300 -0
  25. foodforthought_cli-0.2.4.dist-info/RECORD +44 -0
  26. foodforthought_cli-0.2.4.dist-info/top_level.txt +6 -0
  27. mechdog_labeled/__init__.py +3 -0
  28. mechdog_labeled/primitives.py +113 -0
  29. mechdog_labeled/servo_map.py +209 -0
  30. mechdog_output/__init__.py +3 -0
  31. mechdog_output/primitives.py +59 -0
  32. mechdog_output/servo_map.py +203 -0
  33. test_autodetect/__init__.py +3 -0
  34. test_autodetect/primitives.py +113 -0
  35. test_autodetect/servo_map.py +209 -0
  36. test_full_auto/__init__.py +3 -0
  37. test_full_auto/primitives.py +113 -0
  38. test_full_auto/servo_map.py +209 -0
  39. test_smart_detect/__init__.py +3 -0
  40. test_smart_detect/primitives.py +113 -0
  41. test_smart_detect/servo_map.py +209 -0
  42. foodforthought_cli-0.2.1.dist-info/METADATA +0 -151
  43. foodforthought_cli-0.2.1.dist-info/RECORD +0 -9
  44. foodforthought_cli-0.2.1.dist-info/top_level.txt +0 -1
  45. {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.4.dist-info}/WHEEL +0 -0
  46. {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.4.dist-info}/entry_points.txt +0 -0
ate/mcp_server.py CHANGED
@@ -195,6 +195,158 @@ def get_robot_tools() -> List[Tool]:
195
195
  ]
196
196
 
197
197
 
198
+ def get_marketplace_tools() -> List[Tool]:
199
+ """Unified marketplace tools (Phase 6)"""
200
+ return [
201
+ Tool(
202
+ name="ate_marketplace_robots",
203
+ description="List community robots from the unified marketplace. Includes both Artifex-published and imported robots.",
204
+ inputSchema={
205
+ "type": "object",
206
+ "properties": {
207
+ "search": {
208
+ "type": "string",
209
+ "description": "Search by name or description",
210
+ },
211
+ "category": {
212
+ "type": "string",
213
+ "enum": ["arm", "gripper", "mobile_base", "quadruped", "humanoid",
214
+ "dual_arm", "manipulator", "cobot", "drone", "custom"],
215
+ "description": "Filter by robot category",
216
+ },
217
+ "sort": {
218
+ "type": "string",
219
+ "enum": ["downloads", "rating", "recent", "name"],
220
+ "description": "Sort order",
221
+ "default": "downloads",
222
+ },
223
+ "limit": {
224
+ "type": "number",
225
+ "description": "Maximum results",
226
+ "default": 20,
227
+ },
228
+ },
229
+ },
230
+ ),
231
+ Tool(
232
+ name="ate_marketplace_robot",
233
+ description="Get detailed information about a specific robot from the marketplace, including URDF, links, joints, and parts.",
234
+ inputSchema={
235
+ "type": "object",
236
+ "properties": {
237
+ "robot_id": {
238
+ "type": "string",
239
+ "description": "Robot ID or slug",
240
+ },
241
+ },
242
+ "required": ["robot_id"],
243
+ },
244
+ ),
245
+ Tool(
246
+ name="ate_marketplace_components",
247
+ description="List components from the parts marketplace. Components can be grippers, sensors, actuators, etc.",
248
+ inputSchema={
249
+ "type": "object",
250
+ "properties": {
251
+ "search": {
252
+ "type": "string",
253
+ "description": "Search by name or description",
254
+ },
255
+ "type": {
256
+ "type": "string",
257
+ "enum": ["gripper", "end_effector", "sensor", "camera",
258
+ "actuator", "link", "base", "arm_segment", "custom"],
259
+ "description": "Filter by component type",
260
+ },
261
+ "sort": {
262
+ "type": "string",
263
+ "enum": ["downloads", "rating", "recent", "name"],
264
+ "description": "Sort order",
265
+ "default": "downloads",
266
+ },
267
+ "limit": {
268
+ "type": "number",
269
+ "description": "Maximum results",
270
+ "default": 20,
271
+ },
272
+ },
273
+ },
274
+ ),
275
+ Tool(
276
+ name="ate_marketplace_component",
277
+ description="Get detailed information about a specific component, including compatible robots and specifications.",
278
+ inputSchema={
279
+ "type": "object",
280
+ "properties": {
281
+ "component_id": {
282
+ "type": "string",
283
+ "description": "Component ID",
284
+ },
285
+ },
286
+ "required": ["component_id"],
287
+ },
288
+ ),
289
+ Tool(
290
+ name="ate_skill_transfer_check",
291
+ description="Calculate skill transfer compatibility between robots. Shows which robots can receive skills from a source robot.",
292
+ inputSchema={
293
+ "type": "object",
294
+ "properties": {
295
+ "robot_id": {
296
+ "type": "string",
297
+ "description": "Source robot ID",
298
+ },
299
+ "direction": {
300
+ "type": "string",
301
+ "enum": ["from", "to"],
302
+ "description": "Direction: 'from' = skills from this robot can transfer to others",
303
+ "default": "from",
304
+ },
305
+ "min_score": {
306
+ "type": "number",
307
+ "description": "Minimum compatibility score (0.0-1.0)",
308
+ "default": 0.4,
309
+ },
310
+ "limit": {
311
+ "type": "number",
312
+ "description": "Maximum results",
313
+ "default": 10,
314
+ },
315
+ },
316
+ "required": ["robot_id"],
317
+ },
318
+ ),
319
+ Tool(
320
+ name="ate_robot_parts",
321
+ description="Get parts required by or compatible with a robot.",
322
+ inputSchema={
323
+ "type": "object",
324
+ "properties": {
325
+ "robot_id": {
326
+ "type": "string",
327
+ "description": "Robot ID",
328
+ },
329
+ },
330
+ "required": ["robot_id"],
331
+ },
332
+ ),
333
+ Tool(
334
+ name="ate_component_robots",
335
+ description="Get robots that use or are compatible with a component.",
336
+ inputSchema={
337
+ "type": "object",
338
+ "properties": {
339
+ "component_id": {
340
+ "type": "string",
341
+ "description": "Component ID",
342
+ },
343
+ },
344
+ "required": ["component_id"],
345
+ },
346
+ ),
347
+ ]
348
+
349
+
198
350
  def get_compatibility_tools() -> List[Tool]:
199
351
  """Skill compatibility and adaptation tools"""
200
352
  return [
@@ -798,126 +950,939 @@ def get_test_tools() -> List[Tool]:
798
950
  ]
799
951
 
800
952
 
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
-
837
- @server.call_tool()
838
- async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
839
- """Handle tool calls"""
840
- try:
841
- # Repository tools
842
- if name == "ate_init":
843
- result = client.init(
844
- arguments["name"],
845
- arguments.get("description", ""),
846
- arguments.get("visibility", "public"),
847
- )
848
- return [
849
- TextContent(
850
- type="text",
851
- text=f"Repository created successfully!\nID: {result['repository']['id']}\nName: {result['repository']['name']}",
852
- )
853
- ]
854
-
855
- elif name == "ate_clone":
856
- output = capture_output(
857
- client.clone,
858
- arguments["repo_id"],
859
- arguments.get("target_dir")
860
- )
861
- return [TextContent(type="text", text=output or f"Repository cloned successfully")]
953
+ def get_compiler_tools() -> List[Tool]:
954
+ """
955
+ Skill Compiler Tools - Transform skill.yaml specifications into deployable robot skill packages.
862
956
 
863
- elif name == "ate_list_repositories":
864
- params = {}
865
- if arguments.get("search"):
866
- params["search"] = arguments["search"]
867
- if arguments.get("robot_model"):
868
- params["robotModel"] = arguments["robot_model"]
869
- params["limit"] = arguments.get("limit", 20)
957
+ WORKFLOW FOR AI ASSISTANTS:
958
+ 1. Use ate_list_primitives to discover available building blocks
959
+ 2. Help user create skill.yaml with proper format
960
+ 3. Use ate_validate_skill_spec to check for errors
961
+ 4. Use ate_check_skill_compatibility to verify robot compatibility
962
+ 5. Use ate_compile_skill to generate deployable code
963
+ 6. Use ate_test_compiled_skill to test in dry-run or simulation
964
+ 7. Use ate_publish_compiled_skill to share with community
870
965
 
871
- response = client._request("GET", "/repositories", params=params)
872
- repos = response.get("repositories", [])
966
+ See docs/SKILL_COMPILER.md for full documentation.
967
+ """
968
+ return [
969
+ Tool(
970
+ name="ate_compile_skill",
971
+ description="""Compile a skill.yaml specification into deployable code.
972
+
973
+ WHEN TO USE: After creating and validating a skill.yaml file, use this to generate
974
+ executable Python code, ROS2 packages, or Docker containers.
975
+
976
+ TARGETS:
977
+ - python: Standalone Python package (default, simplest)
978
+ - ros2: ROS2-compatible package with launch files
979
+ - docker: Containerized deployment with Dockerfile
980
+ - all: Generate all formats
981
+
982
+ EXAMPLE:
983
+ {
984
+ "skill_path": "pick_and_place.skill.yaml",
985
+ "target": "python",
986
+ "output": "./dist/pick_and_place"
987
+ }
988
+
989
+ OUTPUT: Creates a directory with generated code, config files, and dependencies.""",
990
+ inputSchema={
991
+ "type": "object",
992
+ "properties": {
993
+ "skill_path": {
994
+ "type": "string",
995
+ "description": "Path to skill.yaml file (e.g., 'skills/pick_place.skill.yaml')",
996
+ },
997
+ "output": {
998
+ "type": "string",
999
+ "description": "Output directory (default: ./output). Will be created if doesn't exist.",
1000
+ },
1001
+ "target": {
1002
+ "type": "string",
1003
+ "enum": ["python", "ros2", "docker", "all"],
1004
+ "description": "Compilation target: python (simplest), ros2 (for ROS2 robots), docker (containerized), all",
1005
+ "default": "python",
1006
+ },
1007
+ "robot": {
1008
+ "type": "string",
1009
+ "description": "Optional: Path to robot URDF for hardware-specific config generation",
1010
+ },
1011
+ },
1012
+ "required": ["skill_path"],
1013
+ },
1014
+ ),
1015
+ Tool(
1016
+ name="ate_test_compiled_skill",
1017
+ description="""Test a compiled skill without deploying to a real robot.
873
1018
 
874
- result_text = f"Found {len(repos)} repositories:\n\n"
875
- for repo in repos[:10]:
876
- result_text += f"- {repo['name']} (ID: {repo['id']})\n"
877
- if repo.get("description"):
878
- result_text += f" {repo['description'][:100]}...\n"
1019
+ WHEN TO USE: After compiling a skill, verify it works correctly before deployment.
879
1020
 
880
- return [TextContent(type="text", text=result_text)]
1021
+ MODES:
1022
+ - dry-run: Traces execution without any robot (fastest, always available)
1023
+ - sim: Runs in simulation (requires simulation setup)
1024
+ - hardware: Runs on real robot (requires robot_port)
881
1025
 
882
- elif name == "ate_get_repository":
883
- response = client._request("GET", f"/repositories/{arguments['repo_id']}")
884
- repo = response.get("repository", {})
1026
+ EXAMPLE:
1027
+ {
1028
+ "skill_path": "./dist/pick_and_place",
1029
+ "mode": "dry-run",
1030
+ "params": {"speed": 0.3, "grip_force": 15.0}
1031
+ }
885
1032
 
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"
1033
+ OUTPUT: Execution trace showing each primitive call and its result.""",
1034
+ inputSchema={
1035
+ "type": "object",
1036
+ "properties": {
1037
+ "skill_path": {
1038
+ "type": "string",
1039
+ "description": "Path to compiled skill directory (output from ate_compile_skill)",
1040
+ },
1041
+ "mode": {
1042
+ "type": "string",
1043
+ "enum": ["sim", "dry-run", "hardware"],
1044
+ "description": "dry-run=trace only, sim=simulation, hardware=real robot",
1045
+ "default": "dry-run",
1046
+ },
1047
+ "robot_port": {
1048
+ "type": "string",
1049
+ "description": "Robot serial port (only for hardware mode, e.g., '/dev/ttyUSB0')",
1050
+ },
1051
+ "params": {
1052
+ "type": "object",
1053
+ "description": "Override skill parameters (e.g., {\"speed\": 0.5})",
1054
+ },
1055
+ },
1056
+ "required": ["skill_path"],
1057
+ },
1058
+ ),
1059
+ Tool(
1060
+ name="ate_publish_compiled_skill",
1061
+ description="""Publish a compiled skill to the FoodForThought registry.
890
1062
 
891
- return [TextContent(type="text", text=result_text)]
1063
+ WHEN TO USE: When the skill is tested and ready to share with others.
892
1064
 
893
- # Robot tools
894
- elif name == "ate_list_robots":
895
- params = {}
896
- if arguments.get("search"):
897
- params["search"] = arguments["search"]
898
- if arguments.get("category"):
899
- params["category"] = arguments["category"]
900
- params["limit"] = arguments.get("limit", 20)
1065
+ VISIBILITY:
1066
+ - public: Anyone can use this skill
1067
+ - private: Only you can see it
1068
+ - team: Shared with your team members
901
1069
 
902
- response = client._request("GET", "/robots/profiles", params=params)
903
- robots = response.get("profiles", [])
1070
+ EXAMPLE:
1071
+ {
1072
+ "skill_path": "./dist/pick_and_place",
1073
+ "visibility": "public"
1074
+ }
904
1075
 
905
- result_text = f"Found {len(robots)} robot profiles:\n\n"
906
- for robot in robots[:10]:
907
- result_text += f"- {robot['modelName']} by {robot['manufacturer']} (ID: {robot['id']})\n"
908
- if robot.get("description"):
909
- result_text += f" {robot['description'][:100]}...\n"
1076
+ OUTPUT: Registry URL where the skill is published.""",
1077
+ inputSchema={
1078
+ "type": "object",
1079
+ "properties": {
1080
+ "skill_path": {
1081
+ "type": "string",
1082
+ "description": "Path to compiled skill directory to publish",
1083
+ },
1084
+ "visibility": {
1085
+ "type": "string",
1086
+ "enum": ["public", "private", "team"],
1087
+ "description": "Who can access this skill",
1088
+ "default": "public",
1089
+ },
1090
+ },
1091
+ "required": ["skill_path"],
1092
+ },
1093
+ ),
1094
+ Tool(
1095
+ name="ate_check_skill_compatibility",
1096
+ description="""Check if a skill can run on a specific robot.
910
1097
 
911
- return [TextContent(type="text", text=result_text)]
1098
+ WHEN TO USE: Before compiling, verify the skill's hardware requirements
1099
+ match the target robot's capabilities (DOF, sensors, payload, etc.).
912
1100
 
913
- elif name == "ate_get_robot":
914
- response = client._request("GET", f"/robots/profiles/{arguments['robot_id']}")
915
- robot = response.get("profile", {})
1101
+ CHECKS PERFORMED:
1102
+ - Arm DOF and reach requirements
1103
+ - Gripper type and force capabilities
1104
+ - Required sensors (cameras, F/T sensors)
1105
+ - Workspace bounds
916
1106
 
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"
1107
+ EXAMPLE:
1108
+ {
1109
+ "skill_path": "pick_and_place.skill.yaml",
1110
+ "robot_urdf": "robots/ur5/ur5.urdf"
1111
+ }
1112
+
1113
+ OUTPUT: Compatibility report with score and list of issues/adaptations needed.""",
1114
+ inputSchema={
1115
+ "type": "object",
1116
+ "properties": {
1117
+ "skill_path": {
1118
+ "type": "string",
1119
+ "description": "Path to skill.yaml file",
1120
+ },
1121
+ "robot_urdf": {
1122
+ "type": "string",
1123
+ "description": "Path to robot URDF file",
1124
+ },
1125
+ "robot_ate_dir": {
1126
+ "type": "string",
1127
+ "description": "Alternative: Path to directory containing ate.yaml robot config",
1128
+ },
1129
+ },
1130
+ "required": ["skill_path"],
1131
+ },
1132
+ ),
1133
+ Tool(
1134
+ name="ate_list_primitives",
1135
+ description="""List available primitives (building blocks) for creating skills.
1136
+
1137
+ WHEN TO USE: When starting to create a skill, or when you need to know what
1138
+ operations are available for a specific hardware type.
1139
+
1140
+ CATEGORIES:
1141
+ - motion: Movement primitives (move_to_pose, move_linear, etc.)
1142
+ - gripper: Gripper actions (open_gripper, close_gripper)
1143
+ - sensing: Sensor reading (capture_image, read_force_torque)
1144
+ - wait: Timing and conditions (wait_time, wait_for_contact)
1145
+ - control: Control modes (set_control_mode, enable_compliance)
1146
+
1147
+ EXAMPLE - List all motion primitives:
1148
+ {
1149
+ "category": "motion"
1150
+ }
1151
+
1152
+ EXAMPLE - List primitives that need a camera:
1153
+ {
1154
+ "hardware": "camera"
1155
+ }
1156
+
1157
+ OUTPUT: List of primitives with their descriptions and parameters.""",
1158
+ inputSchema={
1159
+ "type": "object",
1160
+ "properties": {
1161
+ "category": {
1162
+ "type": "string",
1163
+ "enum": ["motion", "gripper", "sensing", "wait", "control", "all"],
1164
+ "description": "Filter by category (use 'all' to see everything)",
1165
+ "default": "all",
1166
+ },
1167
+ "hardware": {
1168
+ "type": "string",
1169
+ "description": "Filter by hardware type: arm, gripper, camera, force_torque_sensor",
1170
+ },
1171
+ },
1172
+ },
1173
+ ),
1174
+ Tool(
1175
+ name="ate_get_primitive",
1176
+ description="""Get detailed information about a specific primitive.
1177
+
1178
+ WHEN TO USE: When you need to know the exact parameters and requirements
1179
+ for a primitive before using it in a skill.
1180
+
1181
+ COMMON PRIMITIVES:
1182
+ - move_to_pose: Move end-effector to a 7D pose [x,y,z,qx,qy,qz,qw]
1183
+ - move_to_joint_positions: Move to specific joint angles
1184
+ - open_gripper / close_gripper: Gripper control
1185
+ - wait_for_contact: Wait until force threshold is reached
1186
+ - capture_image: Take a camera image
1187
+
1188
+ EXAMPLE:
1189
+ {
1190
+ "name": "move_to_pose"
1191
+ }
1192
+
1193
+ OUTPUT: Full primitive definition with all parameters, types, defaults, and hardware requirements.""",
1194
+ inputSchema={
1195
+ "type": "object",
1196
+ "properties": {
1197
+ "name": {
1198
+ "type": "string",
1199
+ "description": "Primitive name (e.g., 'move_to_pose', 'close_gripper', 'wait_for_contact')",
1200
+ },
1201
+ },
1202
+ "required": ["name"],
1203
+ },
1204
+ ),
1205
+ Tool(
1206
+ name="ate_validate_skill_spec",
1207
+ description="""Validate a skill.yaml file without compiling.
1208
+
1209
+ WHEN TO USE: After creating or modifying a skill.yaml, check for errors before compiling.
1210
+
1211
+ CHECKS PERFORMED:
1212
+ - YAML syntax validity
1213
+ - Required fields (name, version, description, execution)
1214
+ - Parameter type validity
1215
+ - Primitive names exist in registry
1216
+ - Template expression syntax
1217
+ - Hardware requirement format
1218
+
1219
+ EXAMPLE:
1220
+ {
1221
+ "skill_path": "my_skill.skill.yaml"
1222
+ }
1223
+
1224
+ OUTPUT:
1225
+ - If valid: Summary of skill (name, version, parameter count, etc.)
1226
+ - If invalid: List of errors with line numbers and fix suggestions.""",
1227
+ inputSchema={
1228
+ "type": "object",
1229
+ "properties": {
1230
+ "skill_path": {
1231
+ "type": "string",
1232
+ "description": "Path to skill.yaml file to validate",
1233
+ },
1234
+ },
1235
+ "required": ["skill_path"],
1236
+ },
1237
+ ),
1238
+ ]
1239
+
1240
+
1241
+ def get_protocol_tools() -> List[Tool]:
1242
+ """Protocol registry tools"""
1243
+ return [
1244
+ Tool(
1245
+ name="ate_protocol_list",
1246
+ description="List protocols from the FoodForThought protocol registry. Protocols document how to communicate with robot hardware (BLE, serial, WiFi, CAN, etc.)",
1247
+ inputSchema={
1248
+ "type": "object",
1249
+ "properties": {
1250
+ "robot_model": {
1251
+ "type": "string",
1252
+ "description": "Filter by robot model name (e.g., 'mechdog', 'ur5')",
1253
+ },
1254
+ "transport_type": {
1255
+ "type": "string",
1256
+ "enum": ["ble", "serial", "wifi", "can", "i2c", "spi", "mqtt", "ros2"],
1257
+ "description": "Filter by transport type",
1258
+ },
1259
+ "verified_only": {
1260
+ "type": "boolean",
1261
+ "description": "Show only community-verified protocols",
1262
+ "default": False,
1263
+ },
1264
+ "search": {
1265
+ "type": "string",
1266
+ "description": "Search in command format and discovery notes",
1267
+ },
1268
+ },
1269
+ },
1270
+ ),
1271
+ Tool(
1272
+ name="ate_protocol_get",
1273
+ description="Get detailed protocol information including BLE characteristics, serial config, command schema, and associated primitive skills",
1274
+ inputSchema={
1275
+ "type": "object",
1276
+ "properties": {
1277
+ "protocol_id": {
1278
+ "type": "string",
1279
+ "description": "Protocol ID to fetch",
1280
+ },
1281
+ },
1282
+ "required": ["protocol_id"],
1283
+ },
1284
+ ),
1285
+ Tool(
1286
+ name="ate_protocol_init",
1287
+ description="Initialize a new protocol template for a robot. Creates protocol.json and README with transport-specific fields to fill in",
1288
+ inputSchema={
1289
+ "type": "object",
1290
+ "properties": {
1291
+ "robot_model": {
1292
+ "type": "string",
1293
+ "description": "Robot model name (e.g., 'hiwonder-mechdog-pro')",
1294
+ },
1295
+ "transport_type": {
1296
+ "type": "string",
1297
+ "enum": ["ble", "serial", "wifi", "can", "i2c", "spi", "mqtt", "ros2"],
1298
+ "description": "Transport type for communication",
1299
+ },
1300
+ "output_dir": {
1301
+ "type": "string",
1302
+ "description": "Output directory for protocol files",
1303
+ "default": "./protocol",
1304
+ },
1305
+ },
1306
+ "required": ["robot_model", "transport_type"],
1307
+ },
1308
+ ),
1309
+ Tool(
1310
+ name="ate_protocol_push",
1311
+ description="Upload a protocol definition to FoodForThought registry for community use",
1312
+ inputSchema={
1313
+ "type": "object",
1314
+ "properties": {
1315
+ "protocol_file": {
1316
+ "type": "string",
1317
+ "description": "Path to protocol.json file (default: ./protocol.json)",
1318
+ },
1319
+ },
1320
+ },
1321
+ ),
1322
+ Tool(
1323
+ name="ate_protocol_scan_serial",
1324
+ description="Scan for available serial ports on the system. Useful for discovering connected robot hardware",
1325
+ inputSchema={
1326
+ "type": "object",
1327
+ "properties": {},
1328
+ },
1329
+ ),
1330
+ Tool(
1331
+ name="ate_protocol_scan_ble",
1332
+ description="Scan for BLE devices in range. Useful for discovering robot devices before connecting",
1333
+ inputSchema={
1334
+ "type": "object",
1335
+ "properties": {},
1336
+ },
1337
+ ),
1338
+ ]
1339
+
1340
+
1341
+ def get_primitive_tools() -> List[Tool]:
1342
+ """Primitive skills tools"""
1343
+ return [
1344
+ Tool(
1345
+ name="ate_primitive_list",
1346
+ description="List primitive skills - tested atomic robot operations like 'tilt_forward', 'gripper_close', etc. with safe parameter ranges",
1347
+ inputSchema={
1348
+ "type": "object",
1349
+ "properties": {
1350
+ "robot_model": {
1351
+ "type": "string",
1352
+ "description": "Filter by robot model name",
1353
+ },
1354
+ "category": {
1355
+ "type": "string",
1356
+ "enum": ["body_pose", "arm", "gripper", "locomotion", "head", "sensing", "manipulation", "navigation"],
1357
+ "description": "Filter by primitive category",
1358
+ },
1359
+ "status": {
1360
+ "type": "string",
1361
+ "enum": ["experimental", "tested", "verified", "deprecated"],
1362
+ "description": "Filter by status",
1363
+ },
1364
+ "tested_only": {
1365
+ "type": "boolean",
1366
+ "description": "Show only tested/verified primitives",
1367
+ "default": False,
1368
+ },
1369
+ },
1370
+ },
1371
+ ),
1372
+ Tool(
1373
+ name="ate_primitive_get",
1374
+ description="Get detailed primitive skill info including command template, tested parameters with safe ranges, timing, safety notes, and dependencies",
1375
+ inputSchema={
1376
+ "type": "object",
1377
+ "properties": {
1378
+ "primitive_id": {
1379
+ "type": "string",
1380
+ "description": "Primitive skill ID to fetch",
1381
+ },
1382
+ },
1383
+ "required": ["primitive_id"],
1384
+ },
1385
+ ),
1386
+ Tool(
1387
+ name="ate_primitive_test",
1388
+ description="Submit a test result for a primitive skill. Contributes to reliability score and helps verify safe operation ranges",
1389
+ inputSchema={
1390
+ "type": "object",
1391
+ "properties": {
1392
+ "primitive_id": {
1393
+ "type": "string",
1394
+ "description": "Primitive skill ID to test",
1395
+ },
1396
+ "params": {
1397
+ "type": "string",
1398
+ "description": "Parameters used in test as JSON string (e.g., '{\"pitch\": 15}')",
1399
+ },
1400
+ "result": {
1401
+ "type": "string",
1402
+ "enum": ["pass", "fail", "partial"],
1403
+ "description": "Test result",
1404
+ },
1405
+ "notes": {
1406
+ "type": "string",
1407
+ "description": "Additional notes about the test",
1408
+ },
1409
+ "video_url": {
1410
+ "type": "string",
1411
+ "description": "URL to video recording of test",
1412
+ },
1413
+ },
1414
+ "required": ["primitive_id", "params", "result"],
1415
+ },
1416
+ ),
1417
+ Tool(
1418
+ name="ate_primitive_deps_show",
1419
+ description="Show dependency graph for a primitive skill. Shows what primitives it depends on and what depends on it. Indicates deployment readiness",
1420
+ inputSchema={
1421
+ "type": "object",
1422
+ "properties": {
1423
+ "primitive_id": {
1424
+ "type": "string",
1425
+ "description": "Primitive skill ID",
1426
+ },
1427
+ },
1428
+ "required": ["primitive_id"],
1429
+ },
1430
+ ),
1431
+ Tool(
1432
+ name="ate_primitive_deps_add",
1433
+ description="Add a dependency to a primitive skill. Creates deployment gates to ensure required primitives are tested before deployment",
1434
+ inputSchema={
1435
+ "type": "object",
1436
+ "properties": {
1437
+ "primitive_id": {
1438
+ "type": "string",
1439
+ "description": "Primitive skill ID (the one that depends)",
1440
+ },
1441
+ "required_id": {
1442
+ "type": "string",
1443
+ "description": "Required primitive skill ID",
1444
+ },
1445
+ "dependency_type": {
1446
+ "type": "string",
1447
+ "enum": ["requires", "extends", "overrides", "optional"],
1448
+ "description": "Type of dependency",
1449
+ "default": "requires",
1450
+ },
1451
+ "min_status": {
1452
+ "type": "string",
1453
+ "enum": ["experimental", "tested", "verified"],
1454
+ "description": "Minimum required status for deployment",
1455
+ "default": "tested",
1456
+ },
1457
+ },
1458
+ "required": ["primitive_id", "required_id"],
1459
+ },
1460
+ ),
1461
+ ]
1462
+
1463
+
1464
+ def get_bridge_tools() -> List[Tool]:
1465
+ """Robot bridge tools for interactive communication"""
1466
+ return [
1467
+ Tool(
1468
+ name="ate_bridge_scan_serial",
1469
+ description="Scan for available serial ports. Use this to discover connected robots before using bridge connect",
1470
+ inputSchema={
1471
+ "type": "object",
1472
+ "properties": {},
1473
+ "required": [],
1474
+ },
1475
+ ),
1476
+ Tool(
1477
+ name="ate_bridge_scan_ble",
1478
+ description="Scan for BLE devices. Use this to discover bluetooth robots before using bridge connect",
1479
+ inputSchema={
1480
+ "type": "object",
1481
+ "properties": {},
1482
+ "required": [],
1483
+ },
1484
+ ),
1485
+ Tool(
1486
+ name="ate_bridge_send",
1487
+ description="Send a single command to a robot and get the response. Useful for quick tests without opening a full interactive session",
1488
+ inputSchema={
1489
+ "type": "object",
1490
+ "properties": {
1491
+ "port": {
1492
+ "type": "string",
1493
+ "description": "Serial port (e.g., /dev/tty.usbserial-0001) or BLE address",
1494
+ },
1495
+ "command": {
1496
+ "type": "string",
1497
+ "description": "Command to send to the robot",
1498
+ },
1499
+ "transport": {
1500
+ "type": "string",
1501
+ "enum": ["serial", "ble"],
1502
+ "description": "Transport type",
1503
+ "default": "serial",
1504
+ },
1505
+ "baud_rate": {
1506
+ "type": "integer",
1507
+ "description": "Baud rate for serial connection",
1508
+ "default": 115200,
1509
+ },
1510
+ "wait": {
1511
+ "type": "number",
1512
+ "description": "Wait time for response in seconds",
1513
+ "default": 0.5,
1514
+ },
1515
+ },
1516
+ "required": ["port", "command"],
1517
+ },
1518
+ ),
1519
+ Tool(
1520
+ name="ate_bridge_replay",
1521
+ description="Replay a recorded robot session. Useful for testing primitives or reproducing sequences",
1522
+ inputSchema={
1523
+ "type": "object",
1524
+ "properties": {
1525
+ "recording": {
1526
+ "type": "string",
1527
+ "description": "Path to recording JSON file",
1528
+ },
1529
+ "port": {
1530
+ "type": "string",
1531
+ "description": "Serial port or BLE address",
1532
+ },
1533
+ "transport": {
1534
+ "type": "string",
1535
+ "enum": ["serial", "ble"],
1536
+ "description": "Transport type",
1537
+ "default": "serial",
1538
+ },
1539
+ "baud_rate": {
1540
+ "type": "integer",
1541
+ "description": "Baud rate for serial connection",
1542
+ "default": 115200,
1543
+ },
1544
+ "speed": {
1545
+ "type": "number",
1546
+ "description": "Playback speed multiplier (1.0 = normal, 2.0 = 2x speed)",
1547
+ "default": 1.0,
1548
+ },
1549
+ },
1550
+ "required": ["recording", "port"],
1551
+ },
1552
+ ),
1553
+ ]
1554
+
1555
+
1556
+ @server.list_tools()
1557
+ async def list_tools() -> List[Tool]:
1558
+ """List all available MCP tools"""
1559
+ tools = []
1560
+ tools.extend(get_repository_tools())
1561
+ tools.extend(get_robot_tools())
1562
+ tools.extend(get_marketplace_tools()) # Phase 6: Unified marketplace
1563
+ tools.extend(get_compatibility_tools())
1564
+ tools.extend(get_skill_tools())
1565
+ tools.extend(get_protocol_tools())
1566
+ tools.extend(get_primitive_tools())
1567
+ tools.extend(get_bridge_tools())
1568
+ tools.extend(get_parts_tools())
1569
+ tools.extend(get_generate_tools())
1570
+ tools.extend(get_workflow_tools())
1571
+ tools.extend(get_team_tools())
1572
+ tools.extend(get_data_tools())
1573
+ tools.extend(get_deploy_tools())
1574
+ tools.extend(get_test_tools())
1575
+ tools.extend(get_compiler_tools())
1576
+ return tools
1577
+
1578
+
1579
+ # ============================================================================
1580
+ # Tool Handlers
1581
+ # ============================================================================
1582
+
1583
+ def capture_output(func, *args, **kwargs):
1584
+ """Capture printed output from a function"""
1585
+ import io
1586
+ import contextlib
1587
+
1588
+ f = io.StringIO()
1589
+ with contextlib.redirect_stdout(f):
1590
+ try:
1591
+ result = func(*args, **kwargs)
1592
+ except SystemExit:
1593
+ pass # CLI functions may call sys.exit
1594
+ return f.getvalue()
1595
+
1596
+
1597
+ @server.call_tool()
1598
+ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
1599
+ """Handle tool calls"""
1600
+ try:
1601
+ # Repository tools
1602
+ if name == "ate_init":
1603
+ result = client.init(
1604
+ arguments["name"],
1605
+ arguments.get("description", ""),
1606
+ arguments.get("visibility", "public"),
1607
+ )
1608
+ return [
1609
+ TextContent(
1610
+ type="text",
1611
+ text=f"Repository created successfully!\nID: {result['repository']['id']}\nName: {result['repository']['name']}",
1612
+ )
1613
+ ]
1614
+
1615
+ elif name == "ate_clone":
1616
+ output = capture_output(
1617
+ client.clone,
1618
+ arguments["repo_id"],
1619
+ arguments.get("target_dir")
1620
+ )
1621
+ return [TextContent(type="text", text=output or f"Repository cloned successfully")]
1622
+
1623
+ elif name == "ate_list_repositories":
1624
+ params = {}
1625
+ if arguments.get("search"):
1626
+ params["search"] = arguments["search"]
1627
+ if arguments.get("robot_model"):
1628
+ params["robotModel"] = arguments["robot_model"]
1629
+ params["limit"] = arguments.get("limit", 20)
1630
+
1631
+ response = client._request("GET", "/repositories", params=params)
1632
+ repos = response.get("repositories", [])
1633
+
1634
+ result_text = f"Found {len(repos)} repositories:\n\n"
1635
+ for repo in repos[:10]:
1636
+ result_text += f"- {repo['name']} (ID: {repo['id']})\n"
1637
+ if repo.get("description"):
1638
+ result_text += f" {repo['description'][:100]}...\n"
1639
+
1640
+ return [TextContent(type="text", text=result_text)]
1641
+
1642
+ elif name == "ate_get_repository":
1643
+ response = client._request("GET", f"/repositories/{arguments['repo_id']}")
1644
+ repo = response.get("repository", {})
1645
+
1646
+ result_text = f"Repository: {repo.get('name', 'Unknown')}\n"
1647
+ result_text += f"ID: {repo.get('id', 'Unknown')}\n"
1648
+ result_text += f"Description: {repo.get('description', 'No description')}\n"
1649
+ result_text += f"Visibility: {repo.get('visibility', 'unknown')}\n"
1650
+
1651
+ return [TextContent(type="text", text=result_text)]
1652
+
1653
+ # Robot tools
1654
+ elif name == "ate_list_robots":
1655
+ params = {}
1656
+ if arguments.get("search"):
1657
+ params["search"] = arguments["search"]
1658
+ if arguments.get("category"):
1659
+ params["category"] = arguments["category"]
1660
+ params["limit"] = arguments.get("limit", 20)
1661
+
1662
+ response = client._request("GET", "/robots/profiles", params=params)
1663
+ robots = response.get("profiles", [])
1664
+
1665
+ result_text = f"Found {len(robots)} robot profiles:\n\n"
1666
+ for robot in robots[:10]:
1667
+ result_text += f"- {robot['modelName']} by {robot['manufacturer']} (ID: {robot['id']})\n"
1668
+ if robot.get("description"):
1669
+ result_text += f" {robot['description'][:100]}...\n"
1670
+
1671
+ return [TextContent(type="text", text=result_text)]
1672
+
1673
+ elif name == "ate_get_robot":
1674
+ response = client._request("GET", f"/robots/profiles/{arguments['robot_id']}")
1675
+ robot = response.get("profile", {})
1676
+
1677
+ result_text = f"Robot: {robot.get('modelName', 'Unknown')}\n"
1678
+ result_text += f"Manufacturer: {robot.get('manufacturer', 'Unknown')}\n"
1679
+ result_text += f"Category: {robot.get('category', 'Unknown')}\n"
1680
+ result_text += f"Description: {robot.get('description', 'No description')}\n"
1681
+
1682
+ return [TextContent(type="text", text=result_text)]
1683
+
1684
+ # Marketplace tools (Phase 6)
1685
+ elif name == "ate_marketplace_robots":
1686
+ params = {}
1687
+ if arguments.get("search"):
1688
+ params["q"] = arguments["search"]
1689
+ if arguments.get("category"):
1690
+ params["category"] = arguments["category"]
1691
+ if arguments.get("sort"):
1692
+ params["sortBy"] = arguments["sort"]
1693
+ params["limit"] = arguments.get("limit", 20)
1694
+
1695
+ response = client._request("GET", "/robots/unified", params=params)
1696
+ robots = response.get("robots", [])
1697
+
1698
+ if not robots:
1699
+ return [TextContent(type="text", text="No robots found matching your criteria.")]
1700
+
1701
+ result_text = f"Found {len(robots)} robots:\n\n"
1702
+ for robot in robots[:20]:
1703
+ result_text += f"- **{robot.get('name', 'Unknown')}** ({robot.get('manufacturer', 'Unknown')})\n"
1704
+ result_text += f" ID: {robot.get('id')} | Category: {robot.get('category')} | DOF: {robot.get('dof', 'N/A')}\n"
1705
+ if robot.get("description"):
1706
+ result_text += f" {robot['description'][:100]}...\n"
1707
+ result_text += "\n"
1708
+
1709
+ return [TextContent(type="text", text=result_text)]
1710
+
1711
+ elif name == "ate_marketplace_robot":
1712
+ robot_id = arguments["robot_id"]
1713
+ response = client._request("GET", f"/robots/unified/{robot_id}")
1714
+ robot = response.get("robot", {})
1715
+
1716
+ if not robot:
1717
+ return [TextContent(type="text", text=f"Robot not found: {robot_id}")]
1718
+
1719
+ result_text = f"# {robot.get('name', 'Unknown')}\n\n"
1720
+ result_text += f"**Manufacturer:** {robot.get('manufacturer', 'Unknown')}\n"
1721
+ result_text += f"**Category:** {robot.get('category', 'Unknown')}\n"
1722
+ result_text += f"**DOF:** {robot.get('dof', 'N/A')}\n"
1723
+ result_text += f"**Downloads:** {robot.get('downloads', 0)}\n\n"
1724
+
1725
+ if robot.get("description"):
1726
+ result_text += f"## Description\n{robot['description']}\n\n"
1727
+
1728
+ links = robot.get("links", [])
1729
+ if links:
1730
+ result_text += f"## Links ({len(links)})\n"
1731
+ for link in links[:5]:
1732
+ result_text += f"- {link.get('name', 'Unnamed')}\n"
1733
+
1734
+ joints = robot.get("joints", [])
1735
+ if joints:
1736
+ result_text += f"\n## Joints ({len(joints)})\n"
1737
+ for joint in joints[:5]:
1738
+ result_text += f"- {joint.get('name', 'Unnamed')}: {joint.get('type', 'unknown')}\n"
1739
+
1740
+ return [TextContent(type="text", text=result_text)]
1741
+
1742
+ elif name == "ate_marketplace_components":
1743
+ params = {}
1744
+ if arguments.get("search"):
1745
+ params["q"] = arguments["search"]
1746
+ if arguments.get("type"):
1747
+ params["type"] = arguments["type"]
1748
+ if arguments.get("sort"):
1749
+ params["sortBy"] = arguments["sort"]
1750
+ params["limit"] = arguments.get("limit", 20)
1751
+
1752
+ response = client._request("GET", "/components", params=params)
1753
+ components = response.get("components", [])
1754
+
1755
+ if not components:
1756
+ return [TextContent(type="text", text="No components found matching your criteria.")]
1757
+
1758
+ result_text = f"Found {len(components)} components:\n\n"
1759
+ for comp in components[:20]:
1760
+ verified = " ✓" if comp.get("verified") else ""
1761
+ result_text += f"- **{comp.get('name', 'Unknown')}** v{comp.get('version', '1.0')}{verified}\n"
1762
+ result_text += f" ID: {comp.get('id')} | Type: {comp.get('type')} | Downloads: {comp.get('downloads', 0)}\n"
1763
+ if comp.get("description"):
1764
+ result_text += f" {comp['description'][:80]}...\n"
1765
+ result_text += "\n"
1766
+
1767
+ return [TextContent(type="text", text=result_text)]
1768
+
1769
+ elif name == "ate_marketplace_component":
1770
+ component_id = arguments["component_id"]
1771
+ response = client._request("GET", f"/components/{component_id}")
1772
+ comp = response.get("component", {})
1773
+
1774
+ if not comp:
1775
+ return [TextContent(type="text", text=f"Component not found: {component_id}")]
1776
+
1777
+ verified = "Yes ✓" if comp.get("verified") else "No"
1778
+ result_text = f"# {comp.get('name', 'Unknown')}\n\n"
1779
+ result_text += f"**Type:** {comp.get('type', 'Unknown')}\n"
1780
+ result_text += f"**Version:** {comp.get('version', '1.0')}\n"
1781
+ result_text += f"**Verified:** {verified}\n"
1782
+ result_text += f"**Downloads:** {comp.get('downloads', 0)}\n\n"
1783
+
1784
+ if comp.get("description"):
1785
+ result_text += f"## Description\n{comp['description']}\n\n"
1786
+
1787
+ return [TextContent(type="text", text=result_text)]
1788
+
1789
+ elif name == "ate_skill_transfer_check":
1790
+ robot_id = arguments["robot_id"]
1791
+ direction = arguments.get("direction", "from")
1792
+ min_score = arguments.get("min_score", 0.4)
1793
+ limit = arguments.get("limit", 10)
1794
+
1795
+ params = {
1796
+ "direction": direction,
1797
+ "minScore": min_score,
1798
+ "limit": limit,
1799
+ }
1800
+ response = client._request("GET", f"/robots/unified/{robot_id}/skill-transfer", params=params)
1801
+
1802
+ if response.get("error"):
1803
+ return [TextContent(type="text", text=f"Error: {response['error']}")]
1804
+
1805
+ source = response.get("sourceRobot", {})
1806
+ results = response.get("results", [])
1807
+
1808
+ if not results:
1809
+ return [TextContent(type="text", text=f"No compatible robots found for skill transfer from {source.get('name', robot_id)}.")]
1810
+
1811
+ result_text = f"# Skill Transfer Compatibility for {source.get('name', 'Unknown')}\n\n"
1812
+ result_text += f"Direction: Skills can transfer **{direction}** this robot\n\n"
1813
+
1814
+ result_text += f"## Compatible Robots ({len(results)})\n\n"
1815
+ for item in results:
1816
+ robot = item.get("robot", {})
1817
+ scores = item.get("scores", {})
1818
+ adaptation = item.get("adaptationType", "unknown")
1819
+ result_text += f"- **{robot.get('name', 'Unknown')}** - {int(scores.get('overall', 0) * 100)}% ({adaptation})\n"
1820
+ result_text += f" Category: {robot.get('category')} | DOF: {robot.get('dof', 'N/A')}\n\n"
1821
+
1822
+ return [TextContent(type="text", text=result_text)]
1823
+
1824
+ elif name == "ate_robot_parts":
1825
+ robot_id = arguments["robot_id"]
1826
+ response = client._request("GET", f"/robots/unified/{robot_id}/parts")
1827
+
1828
+ if response.get("error"):
1829
+ return [TextContent(type="text", text=f"Error: {response['error']}")]
1830
+
1831
+ robot_name = response.get("robotName", robot_id)
1832
+ required = response.get("requiredParts", [])
1833
+ compatible = response.get("compatibleParts", [])
1834
+
1835
+ result_text = f"# Parts for {robot_name}\n\n"
1836
+
1837
+ if required:
1838
+ result_text += f"## Required Parts ({len(required)})\n"
1839
+ for req in required:
1840
+ comp = req.get("component", {})
1841
+ result_text += f"- **{comp.get('name', 'Unknown')}** ({comp.get('type', 'unknown')})\n"
1842
+ result_text += f" Quantity: {req.get('quantity', 1)} | Required: {'Yes' if req.get('required') else 'Optional'}\n\n"
1843
+ else:
1844
+ result_text += "## Required Parts\nNo required parts specified.\n\n"
1845
+
1846
+ if compatible:
1847
+ result_text += f"## Compatible Parts ({len(compatible)})\n"
1848
+ for compat in compatible[:10]:
1849
+ comp = compat.get("component", {})
1850
+ score = compat.get("compatibilityScore", 0)
1851
+ result_text += f"- **{comp.get('name', 'Unknown')}** ({comp.get('type', 'unknown')})\n"
1852
+ result_text += f" Compatibility: {int(score * 100)}%\n\n"
1853
+
1854
+ return [TextContent(type="text", text=result_text)]
1855
+
1856
+ elif name == "ate_component_robots":
1857
+ component_id = arguments["component_id"]
1858
+ response = client._request("GET", f"/components/{component_id}/compatible-robots")
1859
+
1860
+ if response.get("error"):
1861
+ return [TextContent(type="text", text=f"Error: {response['error']}")]
1862
+
1863
+ comp_name = response.get("componentName", component_id)
1864
+ required_by = response.get("requiredBy", [])
1865
+ compatible_with = response.get("compatibleWith", [])
1866
+
1867
+ result_text = f"# Robots for {comp_name}\n\n"
1868
+
1869
+ if required_by:
1870
+ result_text += f"## Required By ({len(required_by)} robots)\n"
1871
+ for req in required_by:
1872
+ robot = req.get("robot", {})
1873
+ result_text += f"- **{robot.get('name', 'Unknown')}** ({robot.get('category', 'unknown')})\n"
1874
+ result_text += f" Quantity: {req.get('quantity', 1)}\n\n"
1875
+ else:
1876
+ result_text += "## Required By\nNo robots require this component.\n\n"
1877
+
1878
+ if compatible_with:
1879
+ result_text += f"## Compatible With ({len(compatible_with)} robots)\n"
1880
+ for compat in compatible_with[:10]:
1881
+ robot = compat.get("robot", {})
1882
+ score = compat.get("compatibilityScore", 0)
1883
+ verified = " ✓" if compat.get("verified") else ""
1884
+ result_text += f"- **{robot.get('name', 'Unknown')}** ({robot.get('category', 'unknown')})\n"
1885
+ result_text += f" Compatibility: {int(score * 100)}%{verified}\n\n"
921
1886
 
922
1887
  return [TextContent(type="text", text=result_text)]
923
1888
 
@@ -1151,6 +2116,275 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
1151
2116
  )
1152
2117
  return [TextContent(type="text", text=output)]
1153
2118
 
2119
+ # Protocol tools
2120
+ elif name == "ate_protocol_list":
2121
+ output = capture_output(
2122
+ client.protocol_list,
2123
+ arguments.get("robot_model"),
2124
+ arguments.get("transport_type"),
2125
+ arguments.get("verified_only", False),
2126
+ arguments.get("search")
2127
+ )
2128
+ return [TextContent(type="text", text=output)]
2129
+
2130
+ elif name == "ate_protocol_get":
2131
+ output = capture_output(
2132
+ client.protocol_get,
2133
+ arguments["protocol_id"]
2134
+ )
2135
+ return [TextContent(type="text", text=output)]
2136
+
2137
+ elif name == "ate_protocol_init":
2138
+ output = capture_output(
2139
+ client.protocol_init,
2140
+ arguments["robot_model"],
2141
+ arguments["transport_type"],
2142
+ arguments.get("output_dir", "./protocol")
2143
+ )
2144
+ return [TextContent(type="text", text=output)]
2145
+
2146
+ elif name == "ate_protocol_push":
2147
+ output = capture_output(
2148
+ client.protocol_push,
2149
+ arguments.get("protocol_file")
2150
+ )
2151
+ return [TextContent(type="text", text=output)]
2152
+
2153
+ elif name == "ate_protocol_scan_serial":
2154
+ output = capture_output(client.protocol_scan_serial)
2155
+ return [TextContent(type="text", text=output)]
2156
+
2157
+ elif name == "ate_protocol_scan_ble":
2158
+ output = capture_output(client.protocol_scan_ble)
2159
+ return [TextContent(type="text", text=output)]
2160
+
2161
+ # Primitive tools
2162
+ elif name == "ate_primitive_list":
2163
+ output = capture_output(
2164
+ client.primitive_list,
2165
+ arguments.get("robot_model"),
2166
+ arguments.get("category"),
2167
+ arguments.get("status"),
2168
+ arguments.get("tested_only", False)
2169
+ )
2170
+ return [TextContent(type="text", text=output)]
2171
+
2172
+ elif name == "ate_primitive_get":
2173
+ output = capture_output(
2174
+ client.primitive_get,
2175
+ arguments["primitive_id"]
2176
+ )
2177
+ return [TextContent(type="text", text=output)]
2178
+
2179
+ elif name == "ate_primitive_test":
2180
+ output = capture_output(
2181
+ client.primitive_test,
2182
+ arguments["primitive_id"],
2183
+ arguments["params"],
2184
+ arguments["result"],
2185
+ arguments.get("notes"),
2186
+ arguments.get("video_url")
2187
+ )
2188
+ return [TextContent(type="text", text=output)]
2189
+
2190
+ elif name == "ate_primitive_deps_show":
2191
+ output = capture_output(
2192
+ client.primitive_deps_show,
2193
+ arguments["primitive_id"]
2194
+ )
2195
+ return [TextContent(type="text", text=output)]
2196
+
2197
+ elif name == "ate_primitive_deps_add":
2198
+ output = capture_output(
2199
+ client.primitive_deps_add,
2200
+ arguments["primitive_id"],
2201
+ arguments["required_id"],
2202
+ arguments.get("dependency_type", "requires"),
2203
+ arguments.get("min_status", "tested")
2204
+ )
2205
+ return [TextContent(type="text", text=output)]
2206
+
2207
+ # Bridge tools
2208
+ elif name == "ate_bridge_scan_serial":
2209
+ output = capture_output(client.protocol_scan_serial)
2210
+ return [TextContent(type="text", text=output)]
2211
+
2212
+ elif name == "ate_bridge_scan_ble":
2213
+ output = capture_output(client.protocol_scan_ble)
2214
+ return [TextContent(type="text", text=output)]
2215
+
2216
+ elif name == "ate_bridge_send":
2217
+ output = capture_output(
2218
+ client.bridge_send,
2219
+ arguments["port"],
2220
+ arguments["command"],
2221
+ arguments.get("transport", "serial"),
2222
+ arguments.get("baud_rate", 115200),
2223
+ arguments.get("wait", 0.5)
2224
+ )
2225
+ return [TextContent(type="text", text=output if output else "Command sent (no response)")]
2226
+
2227
+ elif name == "ate_bridge_replay":
2228
+ output = capture_output(
2229
+ client.bridge_replay,
2230
+ arguments["recording"],
2231
+ arguments["port"],
2232
+ arguments.get("transport", "serial"),
2233
+ arguments.get("baud_rate", 115200),
2234
+ arguments.get("speed", 1.0)
2235
+ )
2236
+ return [TextContent(type="text", text=output)]
2237
+
2238
+ # Compiler tools
2239
+ elif name == "ate_compile_skill":
2240
+ output = capture_output(
2241
+ client.compile_skill,
2242
+ arguments["skill_path"],
2243
+ arguments.get("output", "./output"),
2244
+ arguments.get("target", "python"),
2245
+ arguments.get("robot"),
2246
+ arguments.get("ate_dir")
2247
+ )
2248
+ return [TextContent(type="text", text=output or "Skill compiled successfully")]
2249
+
2250
+ elif name == "ate_test_compiled_skill":
2251
+ output = capture_output(
2252
+ client.test_compiled_skill,
2253
+ arguments["skill_path"],
2254
+ arguments.get("mode", "dry-run"),
2255
+ arguments.get("robot_port"),
2256
+ arguments.get("params", {})
2257
+ )
2258
+ return [TextContent(type="text", text=output or "Skill test completed")]
2259
+
2260
+ elif name == "ate_publish_compiled_skill":
2261
+ output = capture_output(
2262
+ client.publish_compiled_skill,
2263
+ arguments["skill_path"],
2264
+ arguments.get("visibility", "public")
2265
+ )
2266
+ return [TextContent(type="text", text=output or "Skill published successfully")]
2267
+
2268
+ elif name == "ate_check_skill_compatibility":
2269
+ output = capture_output(
2270
+ client.check_skill_compatibility,
2271
+ arguments["skill_path"],
2272
+ arguments.get("robot_urdf"),
2273
+ arguments.get("robot_ate_dir")
2274
+ )
2275
+ return [TextContent(type="text", text=output or "Compatibility check completed")]
2276
+
2277
+ elif name == "ate_list_primitives":
2278
+ from ate.primitives import PRIMITIVE_REGISTRY, PrimitiveCategory
2279
+
2280
+ category = arguments.get("category", "all")
2281
+ hardware = arguments.get("hardware")
2282
+
2283
+ result_text = "# Available Primitives\n\n"
2284
+
2285
+ for prim_name, prim_def in PRIMITIVE_REGISTRY.items():
2286
+ # Filter by category
2287
+ if category != "all":
2288
+ cat_match = prim_def.get("category", "").lower() == category.lower()
2289
+ if not cat_match:
2290
+ continue
2291
+
2292
+ # Filter by hardware
2293
+ if hardware:
2294
+ req_hardware = prim_def.get("hardware", [])
2295
+ if hardware.lower() not in [h.lower() for h in req_hardware]:
2296
+ continue
2297
+
2298
+ result_text += f"## {prim_name}\n"
2299
+ result_text += f"**Category:** {prim_def.get('category', 'unknown')}\n"
2300
+ result_text += f"**Description:** {prim_def.get('description', 'No description')}\n"
2301
+
2302
+ # Parameters
2303
+ params = prim_def.get("parameters", {})
2304
+ if params:
2305
+ result_text += "**Parameters:**\n"
2306
+ for param_name, param_def in params.items():
2307
+ required = "required" if param_def.get("required", False) else "optional"
2308
+ result_text += f" - `{param_name}` ({param_def.get('type', 'any')}, {required}): {param_def.get('description', '')}\n"
2309
+
2310
+ # Hardware requirements
2311
+ hw_reqs = prim_def.get("hardware", [])
2312
+ if hw_reqs:
2313
+ result_text += f"**Hardware:** {', '.join(hw_reqs)}\n"
2314
+
2315
+ result_text += "\n"
2316
+
2317
+ return [TextContent(type="text", text=result_text)]
2318
+
2319
+ elif name == "ate_get_primitive":
2320
+ from ate.primitives import get_primitive
2321
+
2322
+ prim_name = arguments["name"]
2323
+ prim_def = get_primitive(prim_name)
2324
+
2325
+ if not prim_def:
2326
+ return [TextContent(type="text", text=f"Primitive not found: {prim_name}")]
2327
+
2328
+ result_text = f"# {prim_name}\n\n"
2329
+ result_text += f"**Category:** {prim_def.get('category', 'unknown')}\n"
2330
+ result_text += f"**Description:** {prim_def.get('description', 'No description')}\n\n"
2331
+
2332
+ # Parameters
2333
+ params = prim_def.get("parameters", {})
2334
+ if params:
2335
+ result_text += "## Parameters\n\n"
2336
+ for param_name, param_def in params.items():
2337
+ required = "✓ Required" if param_def.get("required", False) else "○ Optional"
2338
+ default = f", default: `{param_def.get('default')}`" if "default" in param_def else ""
2339
+ result_text += f"### `{param_name}`\n"
2340
+ result_text += f"- **Type:** {param_def.get('type', 'any')}\n"
2341
+ result_text += f"- **Status:** {required}{default}\n"
2342
+ result_text += f"- **Description:** {param_def.get('description', '')}\n\n"
2343
+
2344
+ # Hardware requirements
2345
+ hw_reqs = prim_def.get("hardware", [])
2346
+ if hw_reqs:
2347
+ result_text += f"## Hardware Requirements\n\n"
2348
+ for hw in hw_reqs:
2349
+ result_text += f"- {hw}\n"
2350
+
2351
+ # Return type
2352
+ result_text += f"\n## Returns\n\n`{prim_def.get('returns', 'bool')}`\n"
2353
+
2354
+ return [TextContent(type="text", text=result_text)]
2355
+
2356
+ elif name == "ate_validate_skill_spec":
2357
+ from ate.skill_schema import SkillSpecification
2358
+
2359
+ skill_path = arguments["skill_path"]
2360
+
2361
+ try:
2362
+ spec = SkillSpecification.from_yaml(skill_path)
2363
+ errors = spec.validate()
2364
+
2365
+ if errors:
2366
+ result_text = f"# Validation Failed\n\n"
2367
+ result_text += f"Found {len(errors)} error(s) in `{skill_path}`:\n\n"
2368
+ for error in errors:
2369
+ result_text += f"- ❌ {error}\n"
2370
+ return [TextContent(type="text", text=result_text)]
2371
+
2372
+ result_text = f"# Validation Passed ✓\n\n"
2373
+ result_text += f"**Skill:** {spec.name}\n"
2374
+ result_text += f"**Version:** {spec.version}\n"
2375
+ result_text += f"**Description:** {spec.description}\n\n"
2376
+
2377
+ # Summary
2378
+ result_text += "## Summary\n\n"
2379
+ result_text += f"- **Parameters:** {len(spec.parameters)}\n"
2380
+ result_text += f"- **Hardware Requirements:** {len(spec.hardware_requirements)}\n"
2381
+ result_text += f"- **Execution Steps:** {len(spec.execution)}\n"
2382
+ result_text += f"- **Success Criteria:** {len(spec.success_criteria)}\n"
2383
+
2384
+ return [TextContent(type="text", text=result_text)]
2385
+ except Exception as e:
2386
+ return [TextContent(type="text", text=f"# Validation Error\n\nFailed to parse skill specification:\n\n```\n{str(e)}\n```")]
2387
+
1154
2388
  else:
1155
2389
  return [
1156
2390
  TextContent(