From f341ee267705bf815eaa0d1cc0820ac6360ead55 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Tue, 30 Jun 2026 18:05:54 +0200 Subject: [PATCH] MCP bridge: expose all 16 methods live + fix optional-param required-ness - snaporca_mcp_bridge.py: _param_schema maps array/object types + carries per-param description; _FALLBACK_TOOLS expanded slice-1 -> full 16-method surface (socket-down at client startup still shows the whole toolset) - describe_tools: give genuinely-optional params a default (body -1, profile [], shell.face -1, extrude.distance2 0 / up_to_face -1) so the bridge no longer marks them required (draft.face/edge/path/a/b/reference stay required) Product code byte-identical to snaporca (md5 match). orca_cad slicer builds clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q --- src/slic3r/GUI/McpControl.cpp | 20 +++++------ tools/snaporca_mcp_bridge.py | 65 ++++++++++++++++++++++++++++++----- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/slic3r/GUI/McpControl.cpp b/src/slic3r/GUI/McpControl.cpp index 622848676f..4081133911 100644 --- a/src/slic3r/GUI/McpControl.cpp +++ b/src/slic3r/GUI/McpControl.cpp @@ -104,11 +104,11 @@ json describe_tools() json{{"name", "height"}, {"type", "number"}, {"unit", "mm"}, {"default", 20}, {"min", 0.01}}, json{{"name", "distance"}, {"type", "number"}, {"unit", "mm"}, {"default", 10}, {"min", 0.01}}, json{{"name", "plane"}, {"type", "string"}, {"enum", json::array({"XY", "XZ", "YZ"})}, {"default", "XY"}}, - json{{"name", "profile"}, {"type", "array"}, {"description", "optional closed contour [[x,y],...] in plane mm; overrides width/height"}}, + json{{"name", "profile"}, {"type", "array"}, {"default", json::array()}, {"description", "optional closed contour [[x,y],...] in plane mm; overrides width/height"}}, json{{"name", "boolean"}, {"type", "string"}, {"enum", json::array({"new", "union", "subtract", "intersect"})}, {"default", "new"}}, json{{"name", "end"}, {"type", "string"}, {"enum", json::array({"blind", "symmetric", "two_sided", "through_all", "up_to_face"})}, {"default", "blind"}}, - json{{"name", "distance2"},{"type", "number"}, {"unit", "mm"}, {"description", "second-side depth when end=two_sided"}}, - json{{"name", "up_to_face"},{"type", "integer"}, {"description", "target face id (query_topology on the last body) when end=up_to_face"}}, + json{{"name", "distance2"},{"type", "number"}, {"unit", "mm"}, {"default", 0}, {"description", "second-side depth when end=two_sided (else falls back to distance)"}}, + json{{"name", "up_to_face"},{"type", "integer"}, {"default", -1}, {"description", "target face id (query_topology on the last body) when end=up_to_face"}}, json{{"name", "taper"}, {"type", "number"}, {"unit", "deg"}, {"default", 0}, {"description", "draft/taper of the side wall"}}, json{{"name", "flip"}, {"type", "boolean"}, {"default", false}}, })}}, @@ -120,20 +120,20 @@ json describe_tools() json{{"name", "axis"}, {"type", "integer"}, {"enum", json::array({0, 1})}, {"default", 0}}, json{{"name", "flip"}, {"type", "boolean"}, {"default", false}}, json{{"name", "plane"}, {"type", "string"}, {"enum", json::array({"XY", "XZ", "YZ"})}, {"default", "XY"}}, - json{{"name", "profile"}, {"type", "array"}, {"description", "optional closed contour [[x,y],...] in plane mm; overrides width/height"}}, + json{{"name", "profile"}, {"type", "array"}, {"default", json::array()}, {"description", "optional closed contour [[x,y],...] in plane mm; overrides width/height"}}, json{{"name", "boolean"}, {"type", "string"}, {"enum", json::array({"new", "union", "subtract", "intersect"})}, {"default", "new"}}, })}}, json{{"name", "fillet"}, {"summary", "Round a measured edge of a body (edge id from query_topology on that body)."}, {"params", json::array({ json{{"name", "edge"}, {"type", "integer"}}, json{{"name", "radius"}, {"type", "number"}, {"unit", "mm"}, {"default", 1}, {"min", 0.01}}, - json{{"name", "body"}, {"type", "integer"}, {"description", "target body; omit for the last body. edge id is resolved against THIS body."}}, + json{{"name", "body"}, {"type", "integer"}, {"default", -1}, {"description", "target body; omit for the last body. edge id is resolved against THIS body."}}, })}}, json{{"name", "chamfer"}, {"summary", "Chamfer a measured edge of a body (edge id from query_topology on that body)."}, {"params", json::array({ json{{"name", "edge"}, {"type", "integer"}}, json{{"name", "distance"}, {"type", "number"}, {"unit", "mm"}, {"default", 1}, {"min", 0.01}}, - json{{"name", "body"}, {"type", "integer"}, {"description", "target body; omit for the last body. edge id is resolved against THIS body."}}, + json{{"name", "body"}, {"type", "integer"}, {"default", -1}, {"description", "target body; omit for the last body. edge id is resolved against THIS body."}}, })}}, json{{"name", "hole"}, {"summary", "Drill a circular hole into the current body at (x,y) on a plane."}, {"params", json::array({ @@ -160,19 +160,19 @@ json describe_tools() json{{"name", "dir"}, {"type", "integer"}, {"enum", json::array({0, 1})}, {"default", 0}, {"description", "linear axis: 0=plane X, 1=plane Y"}}, json{{"name", "angle"}, {"type", "number"}, {"unit", "deg"}, {"default", 360}, {"description", "circular total sweep"}}, json{{"name", "plane"}, {"type", "string"}, {"enum", json::array({"XY", "XZ", "YZ"})}, {"default", "XY"}}, - json{{"name", "body"}, {"type", "integer"}, {"description", "target body; omit for the last body"}}, + json{{"name", "body"}, {"type", "integer"}, {"default", -1}, {"description", "target body; omit for the last body"}}, })}}, json{{"name", "shell"}, {"summary", "Hollow a body to a wall thickness (inward); optionally leave one face open."}, {"params", json::array({ json{{"name", "thickness"}, {"type", "number"}, {"unit", "mm"}, {"default", 1}, {"min", 0.01}}, - json{{"name", "face"}, {"type", "integer"}, {"description", "face id to leave open (query_topology); omit for a closed hollow"}}, - json{{"name", "body"}, {"type", "integer"}, {"description", "target body; omit for the last body"}}, + json{{"name", "face"}, {"type", "integer"}, {"default", -1}, {"description", "face id to leave open (query_topology); omit for a closed hollow"}}, + json{{"name", "body"}, {"type", "integer"}, {"default", -1}, {"description", "target body; omit for the last body"}}, })}}, json{{"name", "draft"}, {"summary", "Taper a body face by an angle about its base (pull direction +Z)."}, {"params", json::array({ json{{"name", "face"}, {"type", "integer"}, {"description", "face id to draft (query_topology)"}}, json{{"name", "angle"}, {"type", "number"}, {"unit", "deg"}, {"default", 5}}, - json{{"name", "body"}, {"type", "integer"}, {"description", "target body; omit for the last body"}}, + json{{"name", "body"}, {"type", "integer"}, {"default", -1}, {"description", "target body; omit for the last body"}}, })}}, json{{"name", "query_topology"}, {"summary", "Measured faces (centroid/normal/cylinder) and edges (length/circle) of a body."}, {"params", json::array({ diff --git a/tools/snaporca_mcp_bridge.py b/tools/snaporca_mcp_bridge.py index 60cbb7c240..2976d074ee 100644 --- a/tools/snaporca_mcp_bridge.py +++ b/tools/snaporca_mcp_bridge.py @@ -39,26 +39,75 @@ def app_call(method, params=None): return json.loads(buf.decode()) # --- describe_tools -> MCP tool schemas --------------------------------------- -_TYPE_MAP = {"number": "number", "integer": "integer", "string": "string", "boolean": "boolean"} +_TYPE_MAP = {"number": "number", "integer": "integer", "string": "string", + "boolean": "boolean", "array": "array", "object": "object"} def _param_schema(p): sch = {"type": _TYPE_MAP.get(p.get("type", "string"), "string")} - if "unit" in p: sch["description"] = f"in {p['unit']}" + desc = p.get("description") + if "unit" in p: desc = (desc + " " if desc else "") + f"in {p['unit']}" + if desc: sch["description"] = desc if "enum" in p: sch["enum"] = p["enum"] if "min" in p: sch["minimum"] = p["min"] if "max" in p: sch["maximum"] = p["max"] if "default" in p: sch["default"] = p["default"] return sch -# Slice-1 fallback when the app socket is unreachable at list time. +# Fallback when the app socket is unreachable at list time (e.g. the GUI isn't up yet when the +# MCP client lists tools at session start). The LIVE describe_tools reply is authoritative — this +# mirrors its 16-method surface so the agent still sees the full toolset; params without a +# `default` are surfaced as required. Keep in sync with McpControl.cpp describe_tools(). +def _p(name, typ="number", **kw): return dict(name=name, type=typ, **kw) +_PLANE = _p("plane", "string", enum=["XY", "XZ", "YZ"], default="XY") _FALLBACK_TOOLS = {"tools": [ {"name": "describe_tools", "summary": "List callable tools and their parameters.", "params": []}, {"name": "describe_scene", "summary": "Feature tree + per-body bounding boxes.", "params": []}, - {"name": "extrude", "summary": "Create a solid: a centred rectangle sketch extruded to a depth.", - "params": [{"name": "width", "type": "number", "unit": "mm", "default": 20}, - {"name": "height", "type": "number", "unit": "mm", "default": 20}, - {"name": "distance", "type": "number", "unit": "mm", "default": 10}, - {"name": "plane", "type": "string", "enum": ["XY", "XZ", "YZ"], "default": "XY"}]}, + {"name": "extrude", "summary": "Extrude a profile (or width x height rectangle) to a depth; Onshape end conditions.", + "params": [_p("width", default=20), _p("height", default=20), _p("distance", default=10), _PLANE, + _p("profile", "array", default=[], description="closed [[x,y],...] overrides width/height"), + _p("boolean", "string", enum=["new", "union", "subtract", "intersect"], default="new"), + _p("end", "string", enum=["blind", "symmetric", "two_sided", "through_all", "up_to_face"], default="blind"), + _p("distance2", default=0, description="second side when end=two_sided"), + _p("up_to_face", "integer", default=-1, description="target face id when end=up_to_face"), + _p("taper", default=0), _p("flip", "boolean", default=False)]}, + {"name": "revolve", "summary": "Revolve a profile about a plane axis.", + "params": [_p("width", default=20), _p("height", default=10), _p("angle", default=360), + _p("axis", "integer", enum=[0, 1], default=0), _p("flip", "boolean", default=False), _PLANE, + _p("profile", "array", default=[], description="closed [[x,y],...] overrides width/height"), + _p("boolean", "string", enum=["new", "union", "subtract", "intersect"], default="new")]}, + {"name": "fillet", "summary": "Round a measured edge of a body.", + "params": [_p("edge", "integer"), _p("radius", default=1), + _p("body", "integer", default=-1, description="target body; omit for last")]}, + {"name": "chamfer", "summary": "Chamfer a measured edge of a body.", + "params": [_p("edge", "integer"), _p("distance", default=1), + _p("body", "integer", default=-1, description="target body; omit for last")]}, + {"name": "hole", "summary": "Drill a circular hole at (x,y) on a plane.", + "params": [_p("diameter", default=5), _p("depth", default=10), _p("through", "boolean", default=False), + _p("x", default=0), _p("y", default=0), _PLANE]}, + {"name": "boolean", "summary": "Combine two bodies: union | subtract | intersect.", + "params": [_p("op", "string", enum=["union", "subtract", "intersect"], default="subtract"), + _p("target", "integer", default=0), _p("tool", "integer", default=1), + _p("keep_tool", "boolean", default=False), _p("tolerance", default=0)]}, + {"name": "pattern", "summary": "Replicate a body: linear or circular.", + "params": [_p("circular", "boolean", default=False), _p("count", "integer", default=3), + _p("spacing", default=10), _p("dir", "integer", enum=[0, 1], default=0), + _p("angle", default=360), _PLANE, _p("body", "integer", default=-1, description="target body; omit for last")]}, + {"name": "shell", "summary": "Hollow a body to a wall thickness; optionally open one face.", + "params": [_p("thickness", default=1), _p("face", "integer", default=-1, description="face id to leave open; omit for closed"), + _p("body", "integer", default=-1, description="target body; omit for last")]}, + {"name": "draft", "summary": "Taper a body face by an angle (pull +Z).", + "params": [_p("face", "integer"), _p("angle", default=5), + _p("body", "integer", default=-1, description="target body; omit for last")]}, + {"name": "query_topology", "summary": "Measured faces and edges of a body.", + "params": [_p("body", "integer", default=0)]}, + {"name": "measure", "summary": "Distance/angle between two refs {face|edge|point} on a body.", + "params": [_p("body", "integer", default=0), _p("a", "object"), _p("b", "object")]}, + {"name": "slice_body", "summary": "Cross-section of a body; ordered closed/open contours.", + "params": [_p("body", "integer", default=0), _PLANE, _p("offset", default=0)]}, + {"name": "import_step", "summary": "Import a STEP file as native B-rep bodies.", + "params": [_p("path", "string")]}, + {"name": "validate_against", "summary": "Volume + bbox + surface deviation of a body vs a reference {step|body}.", + "params": [_p("body", "integer", default=0), _p("reference", "object")]}, ]} def list_tools():