@hybridlabor-api/bdb-hardware-pcb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/config/mcp/antigravity.json +22 -0
- package/config/mcp/claude.json +22 -0
- package/config/mcp/codex.toml +25 -0
- package/docs/adr/ADR-001-KICAD-OPENSCAD-MCP-STRATEGY.md +334 -0
- package/docs/review_documentation.md +121 -0
- package/installer.js +358 -0
- package/mcp_servers/kicad-mcp-server/.env.example +22 -0
- package/mcp_servers/kicad-mcp-server/.github/workflows/ci.yml +36 -0
- package/mcp_servers/kicad-mcp-server/CLAUDE.md +487 -0
- package/mcp_servers/kicad-mcp-server/README.md +316 -0
- package/mcp_servers/kicad-mcp-server/docs/DEVICE_TREE.md +416 -0
- package/mcp_servers/kicad-mcp-server/docs/INSTALLATION.md +332 -0
- package/mcp_servers/kicad-mcp-server/docs/PIN_ANALYSIS.md +332 -0
- package/mcp_servers/kicad-mcp-server/docs/README.md +240 -0
- package/mcp_servers/kicad-mcp-server/docs/TESTING.md +613 -0
- package/mcp_servers/kicad-mcp-server/docs/VALIDATION.md +268 -0
- package/mcp_servers/kicad-mcp-server/pyproject.toml +96 -0
- package/mcp_servers/kicad-mcp-server/requirements-dev.txt +16 -0
- package/mcp_servers/kicad-mcp-server/requirements-test.txt +24 -0
- package/mcp_servers/kicad-mcp-server/requirements.txt +13 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__init__.py +3 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__main__.py +17 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/config.py +46 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/types.py +87 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/netlist_parser.py +234 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser.py +375 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser_kicad.py +327 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/schematic_parser.py +902 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/server.py +71 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/arduino/connectivity_test.cpp.j2 +189 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/atmega.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/esp32.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/nrf52.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/stm32f4.dts.j2 +89 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/esp_idf/test_suite.c.j2 +340 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/pytest/test_connectivity.py.j2 +147 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/st_hal/hal_test.c.j2 +313 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_gpio_test.py.j2 +99 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_i2c_test.py.j2 +117 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_pinmux_test.py.j2 +43 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_spi_test.py.j2 +94 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_gpio_test.c.j2 +113 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_i2c_test.c.j2 +101 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_spi_test.c.j2 +94 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/unittest/test_schematic.py.j2 +172 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/__init__.py +36 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/device_tree.py +1187 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/hierarchical_analysis.py +211 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/netlist.py +320 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/parts_registry.py +142 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb.py +955 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb_layout.py +308 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pin_analysis.py +765 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/project.py +196 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic.py +319 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_editor.py +674 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_search.py +158 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/validation.py +866 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/visualization.py +225 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/file_handlers.py +65 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_cli.py +103 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_version.py +103 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/parts_registry.py +197 -0
- package/mcp_servers/kicad-mcp-server/tests/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/examples/ESP32S3_TEST.md +219 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/README.md +65 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/example_pcb.kicad_pcb +177 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/example_schematic.kicad_sch +145 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/child.kicad_sch +24 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/root.kicad_sch +38 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_hierarchical_labels.py +195 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_kicad_cli.py +116 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_netlist_cache_path.py +29 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic.py +189 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic_hierarchy.py +69 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_visualization.py +136 -0
- package/mcp_servers/kicad-mcp-server/uv.lock +2873 -0
- package/mcp_servers/openscad-mcp-server/.dockerignore +9 -0
- package/mcp_servers/openscad-mcp-server/.github/workflows/test.yml +40 -0
- package/mcp_servers/openscad-mcp-server/Dockerfile +29 -0
- package/mcp_servers/openscad-mcp-server/LICENSE +21 -0
- package/mcp_servers/openscad-mcp-server/README.md +154 -0
- package/mcp_servers/openscad-mcp-server/docs/audit.md +56 -0
- package/mcp_servers/openscad-mcp-server/docs/docker.md +66 -0
- package/mcp_servers/openscad-mcp-server/docs/issue-followup.md +19 -0
- package/mcp_servers/openscad-mcp-server/docs/jetson.md +17 -0
- package/mcp_servers/openscad-mcp-server/glama.json +4 -0
- package/mcp_servers/openscad-mcp-server/legacy/README.md +15 -0
- package/mcp_servers/openscad-mcp-server/legacy/README.original.md +294 -0
- package/mcp_servers/openscad-mcp-server/legacy/implementation_plan.md +100 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/download_sam2_checkpoint.py +115 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/ai/sam_segmentation.py +209 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/models/threestudio_generator.py +231 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/workflow/image_to_model_pipeline.py +260 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/test_sam2_segmentation.py +96 -0
- package/mcp_servers/openscad-mcp-server/legacy/requirements.txt +57 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/README.md +39 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/ai-driven-code-generation.md +122 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/export-formats.md +76 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/ai/ai_service.py.md +51 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/main.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/models/code_generator.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/nlp/parameter_extractor.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/ai/natural-language-processing.md +78 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/nlp/parameter-extraction.md +173 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/export-formats.md +91 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/openscad-basics.md +66 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/primitive-testing.md +79 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/ai_service.py +257 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/gemini_api.py +161 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/venice_api.py +203 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/config.py +121 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main.py +1456 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main.py.new +404 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main_remote.py +401 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/code_generator.py +321 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/cuda_mvs.py +209 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/scad_templates/basic_shapes.scad +144 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/nlp/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/nlp/parameter_extractor.py +388 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/wrapper.py +418 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/__init__.py +1 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/printer_discovery.py +471 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/connection_manager.py +537 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_client.py +435 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_server.py +787 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/error_handling.py +415 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/primitive_tester.py +203 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/test_primitives.py +98 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/__init__.py +1 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/cad_exporter.py +241 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/format_validator.py +206 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_exporter.py +140 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_repair.py +91 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_validator.py +123 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/headless_renderer.py +52 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/renderer.py +177 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/web_interface.py +639 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/workflow/image_approval.py +148 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/workflow/multi_view_to_model_pipeline.py +338 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_complete_workflow.py +374 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_cuda_mvs.py +191 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_gemini_api.py +168 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_approval.py +192 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_approval_workflow.py +251 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_to_model_pipeline.py +145 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_model_selection.py +41 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_multi_view_pipeline.py +290 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_primitives.sh +13 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_rabbit_direct.py +71 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_remote_cuda_mvs.py +283 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_venice_example.py +69 -0
- package/mcp_servers/openscad-mcp-server/pyproject.toml +33 -0
- package/mcp_servers/openscad-mcp-server/requirements.txt +2 -0
- package/mcp_servers/openscad-mcp-server/scad/simple_cube.scad +2 -0
- package/mcp_servers/openscad-mcp-server/scripts/test_docker.py +220 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__init__.py +3 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__main__.py +3 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/engine.py +94 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/geometry.py +242 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/server.py +245 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/service.py +190 -0
- package/mcp_servers/openscad-mcp-server/tests/conftest.py +17 -0
- package/mcp_servers/openscad-mcp-server/tests/test_engine.py +37 -0
- package/mcp_servers/openscad-mcp-server/tests/test_geometry.py +106 -0
- package/mcp_servers/openscad-mcp-server/tests/test_integration.py +172 -0
- package/mcp_servers/openscad-mcp-server/tests/test_transports.py +314 -0
- package/mcp_servers/openscad-mcp-server/uv.lock +1123 -0
- package/package.json +44 -0
- package/scripts/install_mcps.sh +86 -0
- package/scripts/openscad_wrapper.sh +62 -0
- package/scripts/run_kicad_mcp.sh +25 -0
- package/scripts/run_openscad_mcp.sh +41 -0
- package/scripts/test_mcp_connection.py +626 -0
- package/scripts/test_mcp_connection.sh +168 -0
- package/skills/code-first-hardware-design/SKILL.md +260 -0
- package/skills/pcb-constraint-definition/SKILL.md +236 -0
- package/skills/pcb-layout-routing-automation/SKILL.md +153 -0
- package/skills/pcb-validation-dfm-signoff/SKILL.md +194 -0
- package/skills/schematic-datasheet-analysis/SKILL.md +202 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Hierarchical schematic analysis tools for KiCad MCP Server."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..parsers.netlist_parser import NetlistParser
|
|
6
|
+
from ..parsers.schematic_parser import SchematicParser
|
|
7
|
+
from ..server import mcp
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _find_component_location(file_path: str, reference: str, sheets: list[dict]) -> tuple[object | None, str]:
|
|
11
|
+
"""Find a component across the main sheet and sub-sheets.
|
|
12
|
+
|
|
13
|
+
Returns (component_or_None, sheet_name).
|
|
14
|
+
"""
|
|
15
|
+
parser = SchematicParser(file_path)
|
|
16
|
+
for component in parser.get_components():
|
|
17
|
+
if component.reference == reference:
|
|
18
|
+
return component, "Main Schematic"
|
|
19
|
+
|
|
20
|
+
project_dir = Path(file_path).parent
|
|
21
|
+
for sheet in sheets:
|
|
22
|
+
sheet_path = project_dir / sheet.get("file", "")
|
|
23
|
+
if sheet_path.exists():
|
|
24
|
+
try:
|
|
25
|
+
for component in SchematicParser(str(sheet_path)).get_components():
|
|
26
|
+
if component.reference == reference:
|
|
27
|
+
return component, sheet.get("name", sheet.get("file", ""))
|
|
28
|
+
except Exception:
|
|
29
|
+
continue
|
|
30
|
+
return None, ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@mcp.tool()
|
|
34
|
+
async def trace_hierarchical_connection(
|
|
35
|
+
file_path: str,
|
|
36
|
+
reference: str,
|
|
37
|
+
pin_number: str = ""
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Trace component connections across hierarchical schematics.
|
|
40
|
+
|
|
41
|
+
This tool traces connections through the entire schematic hierarchy,
|
|
42
|
+
including sub-sheets, to show complete signal paths.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
file_path: Path to main .kicad_sch file
|
|
46
|
+
reference: Component reference designator (e.g., 'U1', 'R5')
|
|
47
|
+
pin_number: Optional pin number to trace (if empty, trace all pins)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Complete connection trace through hierarchy
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
from .pin_analysis import _ensure_netlist
|
|
54
|
+
|
|
55
|
+
parser = SchematicParser(file_path)
|
|
56
|
+
sheets = parser.get_sheets()
|
|
57
|
+
|
|
58
|
+
# Search the main sheet AND sub-sheets (previously only the root was
|
|
59
|
+
# searched, so any component placed in a sub-sheet was "Not Found").
|
|
60
|
+
target_component, location = _find_component_location(file_path, reference, sheets)
|
|
61
|
+
|
|
62
|
+
if not target_component:
|
|
63
|
+
refs = [c.reference for c in parser.get_components()][:10]
|
|
64
|
+
return f"# Component Not Found\n\nComponent '{reference}' not found in schematic hierarchy.\n\nAvailable components: {', '.join(refs)}..."
|
|
65
|
+
|
|
66
|
+
lines = [
|
|
67
|
+
f"# Connection Trace for {target_component.reference}",
|
|
68
|
+
"",
|
|
69
|
+
f"**Component:** {target_component.reference}",
|
|
70
|
+
f"**Value:** {target_component.value}",
|
|
71
|
+
f"**Location:** {location}",
|
|
72
|
+
f"**File:** {file_path}",
|
|
73
|
+
"",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# The authoritative pin<->net map lives in the netlist. SchematicParser
|
|
77
|
+
# nets carry no pin list (SchematicNet.pins is always empty), so tracing
|
|
78
|
+
# via them always reported "no connections".
|
|
79
|
+
netlist_path = await _ensure_netlist(Path(file_path))
|
|
80
|
+
if netlist_path is None:
|
|
81
|
+
lines.append("⚠️ No netlist available — cannot resolve connections. Run `generate_netlist()` first.")
|
|
82
|
+
return "\n".join(lines)
|
|
83
|
+
|
|
84
|
+
nl = NetlistParser(str(netlist_path))
|
|
85
|
+
comp = nl.get_components().get(reference)
|
|
86
|
+
if not comp or not comp.pins:
|
|
87
|
+
lines.append("No connections found for this component.")
|
|
88
|
+
return "\n".join(lines)
|
|
89
|
+
|
|
90
|
+
pins_to_trace = [pin_number] if pin_number else list(comp.pins.keys())
|
|
91
|
+
for pn in pins_to_trace:
|
|
92
|
+
conn = nl.trace_connection(reference, str(pn))
|
|
93
|
+
net_name = conn.get("net") if isinstance(conn, dict) else None
|
|
94
|
+
connected = conn.get("connected_to", []) if isinstance(conn, dict) else []
|
|
95
|
+
lines.append(f"## Pin {pn}")
|
|
96
|
+
lines.append(f"**Net:** {net_name or '(unconnected)'}")
|
|
97
|
+
if connected:
|
|
98
|
+
lines.append(f"**Connected to ({len(connected)} pins):**")
|
|
99
|
+
for ref, p in connected[:15]:
|
|
100
|
+
lines.append(f" - {ref}:{p}")
|
|
101
|
+
if len(connected) > 15:
|
|
102
|
+
lines.append(f" - ... and {len(connected) - 15} more")
|
|
103
|
+
else:
|
|
104
|
+
lines.append("**No external connections**")
|
|
105
|
+
lines.append("")
|
|
106
|
+
|
|
107
|
+
return "\n".join(lines)
|
|
108
|
+
|
|
109
|
+
except FileNotFoundError:
|
|
110
|
+
return f"Error: File not found: {file_path}"
|
|
111
|
+
except Exception as e:
|
|
112
|
+
return f"Error: {e}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@mcp.tool()
|
|
116
|
+
async def analyze_hierarchical_nets(
|
|
117
|
+
file_path: str,
|
|
118
|
+
filter_pattern: str = "",
|
|
119
|
+
show_hierarchy: bool = True
|
|
120
|
+
) -> str:
|
|
121
|
+
"""Analyze all nets in hierarchical schematic design.
|
|
122
|
+
|
|
123
|
+
This tool analyzes network connections across the entire hierarchy,
|
|
124
|
+
showing how signals flow between main schematic and sub-sheets.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
file_path: Path to main .kicad_sch file
|
|
128
|
+
filter_pattern: Optional regex pattern to filter net names
|
|
129
|
+
show_hierarchy: Whether to show hierarchical structure (default: True)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Complete hierarchical net analysis
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
import re
|
|
136
|
+
|
|
137
|
+
from .pin_analysis import _ensure_netlist
|
|
138
|
+
|
|
139
|
+
parser = SchematicParser(file_path)
|
|
140
|
+
sheets = parser.get_sheets()
|
|
141
|
+
|
|
142
|
+
netlist_path = await _ensure_netlist(Path(file_path))
|
|
143
|
+
if netlist_path is None:
|
|
144
|
+
return "# No Netlist Available\n\nCannot analyze nets without a netlist. Run `generate_netlist()` first."
|
|
145
|
+
|
|
146
|
+
nl = NetlistParser(str(netlist_path))
|
|
147
|
+
|
|
148
|
+
# docstring promises a regex; honour it (was a plain substring check).
|
|
149
|
+
try:
|
|
150
|
+
flt = re.compile(filter_pattern, re.IGNORECASE) if filter_pattern else None
|
|
151
|
+
except re.error:
|
|
152
|
+
flt = None
|
|
153
|
+
|
|
154
|
+
# NetlistNet.pins is a list[(ref, pin)] that is actually populated,
|
|
155
|
+
# unlike SchematicNet.pins which is always empty.
|
|
156
|
+
hierarchical_nets = {}
|
|
157
|
+
for name, net in nl.get_nets().items():
|
|
158
|
+
if flt and not flt.search(name):
|
|
159
|
+
continue
|
|
160
|
+
hierarchical_nets[name] = {
|
|
161
|
+
"location": "Netlist (whole board)",
|
|
162
|
+
"pins": net.pins,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if not hierarchical_nets:
|
|
166
|
+
return f"# No Nets Found\n\nNo nets found matching pattern: {filter_pattern if filter_pattern else 'all'}"
|
|
167
|
+
|
|
168
|
+
lines = [
|
|
169
|
+
"# Hierarchical Net Analysis",
|
|
170
|
+
"",
|
|
171
|
+
f"**File:** {file_path}",
|
|
172
|
+
f"**Total nets:** {len(hierarchical_nets)}",
|
|
173
|
+
f"**Filter:** {filter_pattern if filter_pattern else 'none'}",
|
|
174
|
+
f"**Hierarchical sheets:** {len(sheets)}",
|
|
175
|
+
"",
|
|
176
|
+
"## Network Summary",
|
|
177
|
+
"",
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
power_nets, signal_nets, interface_nets = [], [], []
|
|
181
|
+
for net_name, net_info in hierarchical_nets.items():
|
|
182
|
+
up = net_name.upper()
|
|
183
|
+
if any(p in up for p in ["VDD", "VSS", "GND", "VCC", "VBAT", "VPP", "+3V", "+5V"]):
|
|
184
|
+
power_nets.append((net_name, net_info))
|
|
185
|
+
elif any(i in up for i in ["I2C", "SPI", "UART", "SDA", "SCL", "MOSI", "MISO", "TX", "RX"]):
|
|
186
|
+
interface_nets.append((net_name, net_info))
|
|
187
|
+
else:
|
|
188
|
+
signal_nets.append((net_name, net_info))
|
|
189
|
+
|
|
190
|
+
def _emit(title: str, group: list, limit: int) -> None:
|
|
191
|
+
if not group:
|
|
192
|
+
return
|
|
193
|
+
lines.append(f"### {title}")
|
|
194
|
+
for net_name, net_info in sorted(group)[:limit]:
|
|
195
|
+
lines.append(f"**{net_name}** ({net_info['location']})")
|
|
196
|
+
lines.append(f" Connections: {len(net_info['pins'])} pins")
|
|
197
|
+
lines.append("")
|
|
198
|
+
if len(group) > limit:
|
|
199
|
+
lines.append(f"*... and {len(group) - limit} more*")
|
|
200
|
+
lines.append("")
|
|
201
|
+
|
|
202
|
+
_emit("Power Nets", power_nets, 20)
|
|
203
|
+
_emit("Interface Nets", interface_nets, 30)
|
|
204
|
+
_emit(f"Signal Nets (showing first 20 of {len(signal_nets)})", signal_nets, 20)
|
|
205
|
+
|
|
206
|
+
return "\n".join(lines)
|
|
207
|
+
|
|
208
|
+
except FileNotFoundError:
|
|
209
|
+
return f"Error: File not found: {file_path}"
|
|
210
|
+
except Exception as e:
|
|
211
|
+
return f"Error: {e}"
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Netlist generation and analysis tools for KiCad MCP Server."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..parsers.netlist_parser import NetlistParser
|
|
8
|
+
from ..server import mcp
|
|
9
|
+
from ..utils.kicad_cli import run_kicad_cli
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def netlist_cache_path(sch_path: Path) -> Path:
|
|
13
|
+
"""Deterministic per-schematic netlist path in the temp directory.
|
|
14
|
+
|
|
15
|
+
Keying only on the file stem made two different projects that both have
|
|
16
|
+
e.g. a ``main.kicad_sch`` overwrite each other's exported netlist, so a
|
|
17
|
+
short digest of the resolved schematic path is mixed into the name.
|
|
18
|
+
"""
|
|
19
|
+
digest = hashlib.sha256(str(sch_path.resolve()).encode("utf-8")).hexdigest()[:8]
|
|
20
|
+
return Path(tempfile.gettempdir()) / f"{sch_path.stem}_{digest}.xml"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _find_root_schematic(sch_path: Path) -> Path | None:
|
|
24
|
+
"""If sch_path is a sub-sheet, return the root schematic instead.
|
|
25
|
+
|
|
26
|
+
KiCad convention: root schematic has the same stem as the .kicad_pro file.
|
|
27
|
+
Sub-sheets lack the hierarchy context needed to resolve unnamed wire-only
|
|
28
|
+
nets, so we redirect to the root schematic for complete netlist export.
|
|
29
|
+
"""
|
|
30
|
+
pro_files = list(sch_path.parent.glob("*.kicad_pro"))
|
|
31
|
+
if len(pro_files) != 1:
|
|
32
|
+
return None
|
|
33
|
+
root_sch = pro_files[0].with_suffix(".kicad_sch")
|
|
34
|
+
if root_sch.exists() and root_sch.resolve() != sch_path.resolve():
|
|
35
|
+
return root_sch
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@mcp.tool()
|
|
40
|
+
async def generate_netlist(
|
|
41
|
+
schematic_path: str,
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Generate KiCad netlist from schematic file.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
schematic_path: Path to .kicad_sch file
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Path to generated .xml netlist file
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
sch_path = Path(schematic_path)
|
|
53
|
+
if not sch_path.exists():
|
|
54
|
+
return f"❌ Schematic file not found: {schematic_path}"
|
|
55
|
+
|
|
56
|
+
# Detect sub-sheet and redirect to root schematic.
|
|
57
|
+
# Sub-sheets lack hierarchy context to resolve unnamed wire-only nets.
|
|
58
|
+
subsheet_note = ""
|
|
59
|
+
root_sch = _find_root_schematic(sch_path)
|
|
60
|
+
if root_sch:
|
|
61
|
+
subsheet_note = (
|
|
62
|
+
f"\n\n⚠️ **Note:** `{sch_path.name}` is a hierarchical sub-sheet. "
|
|
63
|
+
f"Switched to root schematic `{root_sch.name}` for complete netlist "
|
|
64
|
+
f"(unnamed wire-only nets require full hierarchy context to resolve)."
|
|
65
|
+
)
|
|
66
|
+
sch_path = root_sch
|
|
67
|
+
|
|
68
|
+
# KiCad 7+ uses kicad-cli for headless netlist export
|
|
69
|
+
# Output to temp directory to avoid read-only volume issues
|
|
70
|
+
netlist_path = netlist_cache_path(sch_path)
|
|
71
|
+
|
|
72
|
+
# Try to use KiCad's netlist export
|
|
73
|
+
# Note: Requires a KiCad installation — kicad-cli is located via
|
|
74
|
+
# PATH or the detected install directory (see utils.kicad_cli).
|
|
75
|
+
try:
|
|
76
|
+
# Use kicad-cli for headless export (no display required)
|
|
77
|
+
result = await run_kicad_cli(
|
|
78
|
+
[
|
|
79
|
+
"sch",
|
|
80
|
+
"export",
|
|
81
|
+
"netlist",
|
|
82
|
+
"--format",
|
|
83
|
+
"kicadxml",
|
|
84
|
+
"--output",
|
|
85
|
+
str(netlist_path),
|
|
86
|
+
str(sch_path),
|
|
87
|
+
],
|
|
88
|
+
timeout=30,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if result.returncode == 0 and netlist_path.exists():
|
|
92
|
+
return f"""✅ Netlist generated successfully
|
|
93
|
+
|
|
94
|
+
**Output:** {netlist_path}
|
|
95
|
+
|
|
96
|
+
You can now use netlist-based tools:
|
|
97
|
+
- `trace_netlist_connection()` - Trace component connections via netlist
|
|
98
|
+
- `get_netlist_nets()` - List all nets
|
|
99
|
+
- `get_netlist_components()` - List all components with their nets
|
|
100
|
+
{subsheet_note}"""
|
|
101
|
+
else:
|
|
102
|
+
# Fallback: return instructions
|
|
103
|
+
return f"""⚠️ Automatic netlist generation failed.
|
|
104
|
+
|
|
105
|
+
KiCad command line tools may not be available.
|
|
106
|
+
|
|
107
|
+
**Manual export steps:**
|
|
108
|
+
1. Open schematic in KiCad: {sch_path}
|
|
109
|
+
2. Go to: Tools → Generate Netlist
|
|
110
|
+
3. Select: "KiCad XML" format
|
|
111
|
+
4. Save to: {netlist_path}
|
|
112
|
+
|
|
113
|
+
**Alternative:**
|
|
114
|
+
Use KiCad Schematic Editor GUI to export netlist.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
except FileNotFoundError:
|
|
118
|
+
return """⚠️ kicad-cli not found (PATH or KiCad install directory).
|
|
119
|
+
|
|
120
|
+
Please:
|
|
121
|
+
1. Install KiCad 7+ (https://www.kicad.org/)
|
|
122
|
+
2. Ensure kicad-cli is in system PATH, or set the KICAD_CLI
|
|
123
|
+
environment variable to its full path
|
|
124
|
+
3. Or use KiCad GUI to export netlist manually
|
|
125
|
+
|
|
126
|
+
**Manual export:**
|
|
127
|
+
Open schematic → Tools → Generate Netlist → KiCad XML format
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
return f"❌ Error generating netlist: {e}"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@mcp.tool()
|
|
135
|
+
async def trace_netlist_connection(
|
|
136
|
+
netlist_path: str,
|
|
137
|
+
reference: str,
|
|
138
|
+
pin_number: str = "",
|
|
139
|
+
) -> str:
|
|
140
|
+
"""Trace component connections using netlist file (most accurate).
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
netlist_path: Path to .xml netlist file
|
|
144
|
+
reference: Component reference (e.g., 'R16')
|
|
145
|
+
pin_number: Optional pin number (e.g., '1'). If empty, trace all pins.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Detailed connection information from netlist
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
parser = NetlistParser(netlist_path)
|
|
152
|
+
|
|
153
|
+
if pin_number:
|
|
154
|
+
result = parser.trace_connection(reference, pin_number)
|
|
155
|
+
|
|
156
|
+
if "error" in result:
|
|
157
|
+
return f"❌ {result['error']}"
|
|
158
|
+
|
|
159
|
+
lines = [
|
|
160
|
+
f"## Netlist Connection Trace: {reference}",
|
|
161
|
+
"",
|
|
162
|
+
f"**Pin:** {result['pin']}",
|
|
163
|
+
f"**Net:** {result['net']}",
|
|
164
|
+
"",
|
|
165
|
+
"### Connected Components:",
|
|
166
|
+
"",
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
if result['connected_to']:
|
|
170
|
+
lines.append("| Reference | Pin |")
|
|
171
|
+
lines.append("|-----------|-----|")
|
|
172
|
+
for ref, pin in result['connected_to']:
|
|
173
|
+
lines.append(f"| {ref} | {pin} |")
|
|
174
|
+
else:
|
|
175
|
+
lines.append("No other components on this net.")
|
|
176
|
+
|
|
177
|
+
return "\n".join(lines)
|
|
178
|
+
|
|
179
|
+
else:
|
|
180
|
+
result = parser.trace_connection(reference)
|
|
181
|
+
|
|
182
|
+
if "error" in result:
|
|
183
|
+
return f"❌ {result['error']}"
|
|
184
|
+
|
|
185
|
+
lines = [
|
|
186
|
+
f"## Netlist Connection Trace: {reference}",
|
|
187
|
+
"",
|
|
188
|
+
f"**Total Nets:** {len(result['nets'])}",
|
|
189
|
+
"",
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
for net_name, net_info in result['nets'].items():
|
|
193
|
+
lines.append(f"### Net: {net_name}")
|
|
194
|
+
lines.append(f"**Pin:** {net_info['pin']}")
|
|
195
|
+
lines.append("")
|
|
196
|
+
|
|
197
|
+
if net_info['connected_to']:
|
|
198
|
+
lines.append("| Reference | Pin |")
|
|
199
|
+
lines.append("|-----------|-----|")
|
|
200
|
+
for ref, pin in net_info['connected_to']:
|
|
201
|
+
lines.append(f"| {ref} | {pin} |")
|
|
202
|
+
else:
|
|
203
|
+
lines.append("No other components on this net.")
|
|
204
|
+
|
|
205
|
+
lines.append("")
|
|
206
|
+
|
|
207
|
+
return "\n".join(lines)
|
|
208
|
+
|
|
209
|
+
except FileNotFoundError as e:
|
|
210
|
+
return f"❌ File not found: {e}"
|
|
211
|
+
except Exception as e:
|
|
212
|
+
return f"❌ Error tracing netlist: {e}"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@mcp.tool()
|
|
216
|
+
async def get_netlist_nets(
|
|
217
|
+
netlist_path: str,
|
|
218
|
+
filter_pattern: str = "",
|
|
219
|
+
) -> str:
|
|
220
|
+
"""Get all nets from netlist file.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
netlist_path: Path to .xml netlist file
|
|
224
|
+
filter_pattern: Optional regex pattern to filter net names
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
List of all nets with their connections
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
parser = NetlistParser(netlist_path)
|
|
231
|
+
nets = parser.get_nets()
|
|
232
|
+
|
|
233
|
+
lines = [
|
|
234
|
+
f"## Nets from Netlist: {netlist_path}",
|
|
235
|
+
"",
|
|
236
|
+
f"**Total Nets:** {len(nets)}",
|
|
237
|
+
"",
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
for net_name, net in sorted(nets.items()):
|
|
241
|
+
if filter_pattern:
|
|
242
|
+
import re
|
|
243
|
+
if not re.search(filter_pattern, net_name, re.IGNORECASE):
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
lines.append(f"### {net_name}")
|
|
247
|
+
lines.append(f"**Code:** {net.code}")
|
|
248
|
+
lines.append(f"**Connections:** {len(net.pins)} pins")
|
|
249
|
+
|
|
250
|
+
if net.pins:
|
|
251
|
+
lines.append("")
|
|
252
|
+
lines.append("| Reference | Pin |")
|
|
253
|
+
lines.append("|-----------|-----|")
|
|
254
|
+
for ref, pin in net.pins[:10]: # Show first 10
|
|
255
|
+
lines.append(f"| {ref} | {pin} |")
|
|
256
|
+
if len(net.pins) > 10:
|
|
257
|
+
lines.append(f"| ... | ({len(net.pins) - 10} more) |")
|
|
258
|
+
|
|
259
|
+
lines.append("")
|
|
260
|
+
|
|
261
|
+
return "\n".join(lines)
|
|
262
|
+
|
|
263
|
+
except FileNotFoundError as e:
|
|
264
|
+
return f"❌ File not found: {e}"
|
|
265
|
+
except Exception as e:
|
|
266
|
+
return f"❌ Error reading netlist: {e}"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@mcp.tool()
|
|
270
|
+
async def get_netlist_components(
|
|
271
|
+
netlist_path: str,
|
|
272
|
+
filter_ref: str = "",
|
|
273
|
+
) -> str:
|
|
274
|
+
"""Get all components from netlist with their network connections.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
netlist_path: Path to .xml netlist file
|
|
278
|
+
filter_ref: Optional reference filter (e.g., 'R', 'U', 'C')
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
List of components with their net connections
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
parser = NetlistParser(netlist_path)
|
|
285
|
+
components = parser.get_components()
|
|
286
|
+
|
|
287
|
+
lines = [
|
|
288
|
+
f"## Components from Netlist: {netlist_path}",
|
|
289
|
+
"",
|
|
290
|
+
f"**Total Components:** {len(components)}",
|
|
291
|
+
"",
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
for ref, comp in sorted(components.items()):
|
|
295
|
+
if filter_ref and not ref.startswith(filter_ref):
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
lines.append(f"### {ref}")
|
|
299
|
+
lines.append(f"**Value:** {comp.value}")
|
|
300
|
+
lines.append(f"**Library:** {comp.library}")
|
|
301
|
+
if comp.footprint:
|
|
302
|
+
lines.append(f"**Footprint:** {comp.footprint}")
|
|
303
|
+
|
|
304
|
+
if comp.pins:
|
|
305
|
+
lines.append("")
|
|
306
|
+
lines.append("**Pin Connections:**")
|
|
307
|
+
lines.append("")
|
|
308
|
+
lines.append("| Pin | Net |")
|
|
309
|
+
lines.append("|-----|-----|")
|
|
310
|
+
for pin_num, net_name in sorted(comp.pins.items()):
|
|
311
|
+
lines.append(f"| {pin_num} | {net_name} |")
|
|
312
|
+
|
|
313
|
+
lines.append("")
|
|
314
|
+
|
|
315
|
+
return "\n".join(lines)
|
|
316
|
+
|
|
317
|
+
except FileNotFoundError as e:
|
|
318
|
+
return f"❌ File not found: {e}"
|
|
319
|
+
except Exception as e:
|
|
320
|
+
return f"❌ Error reading netlist: {e}"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Parts registry tools — search and fetch verified KiCad parts.
|
|
2
|
+
|
|
3
|
+
Backed by an open, no-login registry (default: PartReel, https://partreel.com;
|
|
4
|
+
configurable via PARTS_REGISTRY_URL). Lets the assistant pull real, quality-
|
|
5
|
+
gated footprints/symbols/3D models instead of drawing them from scratch.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ..server import mcp
|
|
14
|
+
from ..utils.parts_registry import (
|
|
15
|
+
FORMAT_EXTENSIONS,
|
|
16
|
+
RegistryClient,
|
|
17
|
+
RegistryError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@mcp.tool()
|
|
22
|
+
async def search_parts_registry(query: str, limit: int = 20) -> str:
|
|
23
|
+
"""Search an open registry of 21,000+ verified KiCad parts (no login).
|
|
24
|
+
|
|
25
|
+
Matches part id, name, family, category, manufacturer and keywords.
|
|
26
|
+
Example queries: "usb c connector", "0402 capacitor", "nrf9151".
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
query: Search terms (all terms must match; '*' wildcards allowed)
|
|
30
|
+
limit: Maximum number of results (default 20)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
JSON list of matches (id, name, family, page URL)
|
|
34
|
+
"""
|
|
35
|
+
client = RegistryClient()
|
|
36
|
+
try:
|
|
37
|
+
hits = await asyncio.to_thread(client.search, query, limit)
|
|
38
|
+
except RegistryError as exc:
|
|
39
|
+
return f"❌ Registry search failed: {exc}"
|
|
40
|
+
if not hits:
|
|
41
|
+
return f"No registry parts matched '{query}'."
|
|
42
|
+
rows = [
|
|
43
|
+
{
|
|
44
|
+
"id": p.get("id"),
|
|
45
|
+
"name": p.get("name"),
|
|
46
|
+
"family": p.get("family"),
|
|
47
|
+
"category": p.get("category"),
|
|
48
|
+
"manufacturer": p.get("manufacturer"),
|
|
49
|
+
"page": p.get("page"),
|
|
50
|
+
}
|
|
51
|
+
for p in hits
|
|
52
|
+
]
|
|
53
|
+
return json.dumps({"count": len(rows), "parts": rows}, indent=2)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@mcp.tool()
|
|
57
|
+
async def get_registry_part(part_id: str) -> str:
|
|
58
|
+
"""Get the full registry record for one part.
|
|
59
|
+
|
|
60
|
+
Includes download URLs (footprint/symbol/3D), datasheet link, dimension
|
|
61
|
+
provenance and field reports from real builds.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
part_id: Registry part id (from search_parts_registry)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
JSON part record
|
|
68
|
+
"""
|
|
69
|
+
client = RegistryClient()
|
|
70
|
+
try:
|
|
71
|
+
part = await asyncio.to_thread(client.get_part, part_id)
|
|
72
|
+
except RegistryError as exc:
|
|
73
|
+
return f"❌ Registry lookup failed: {exc}"
|
|
74
|
+
return json.dumps(part, indent=2)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@mcp.tool()
|
|
78
|
+
async def download_registry_part(
|
|
79
|
+
part_id: str, save_dir: str, formats: list[str] | None = None
|
|
80
|
+
) -> str:
|
|
81
|
+
"""Download a registry part's files into a local directory.
|
|
82
|
+
|
|
83
|
+
Downloads are HTTPS-only, restricted to the registry's own hosts,
|
|
84
|
+
capped at 50 MB per file, and saved with extension-checked filenames.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
part_id: Registry part id (from search_parts_registry)
|
|
88
|
+
save_dir: Existing local directory (e.g. the project library folder)
|
|
89
|
+
formats: Which files — any of "footprint", "symbol", "step",
|
|
90
|
+
"preview" (default: footprint + symbol)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Saved file paths, or an error
|
|
94
|
+
"""
|
|
95
|
+
formats = formats or ["footprint", "symbol"]
|
|
96
|
+
unknown = [f for f in formats if f not in FORMAT_EXTENSIONS]
|
|
97
|
+
if unknown:
|
|
98
|
+
return f"❌ Unknown formats: {unknown} (allowed: {sorted(FORMAT_EXTENSIONS)})"
|
|
99
|
+
|
|
100
|
+
dest = Path(save_dir)
|
|
101
|
+
if not dest.is_dir():
|
|
102
|
+
return f"❌ Not a directory: {save_dir}"
|
|
103
|
+
# Confine writes to the working dir or the user's home tree
|
|
104
|
+
# (extend via PARTS_REGISTRY_SAVE_ROOTS, comma-separated).
|
|
105
|
+
roots = [Path.cwd(), Path.home()]
|
|
106
|
+
roots += [
|
|
107
|
+
Path(r.strip())
|
|
108
|
+
for r in os.environ.get("PARTS_REGISTRY_SAVE_ROOTS", "").split(",")
|
|
109
|
+
if r.strip()
|
|
110
|
+
]
|
|
111
|
+
real = dest.resolve()
|
|
112
|
+
if not any(real == r.resolve() or r.resolve() in real.parents for r in roots):
|
|
113
|
+
return f"❌ save_dir outside allowed roots (cwd/home): {save_dir}"
|
|
114
|
+
|
|
115
|
+
client = RegistryClient()
|
|
116
|
+
try:
|
|
117
|
+
part = await asyncio.to_thread(client.get_part, part_id)
|
|
118
|
+
except RegistryError as exc:
|
|
119
|
+
return f"❌ Registry lookup failed: {exc}"
|
|
120
|
+
|
|
121
|
+
files = (part.get("files") if isinstance(part, dict) else None) or {}
|
|
122
|
+
saved, missing = [], []
|
|
123
|
+
for fmt in formats:
|
|
124
|
+
url = files.get(fmt)
|
|
125
|
+
if not url:
|
|
126
|
+
missing.append(fmt)
|
|
127
|
+
continue
|
|
128
|
+
try:
|
|
129
|
+
path = await asyncio.to_thread(
|
|
130
|
+
client.download_asset, url, str(real), part_id, fmt
|
|
131
|
+
)
|
|
132
|
+
saved.append(path)
|
|
133
|
+
except (RegistryError, OSError) as exc:
|
|
134
|
+
# OSError covers disk-full / permission-denied / Windows reserved
|
|
135
|
+
# names — surface the partial result instead of losing it.
|
|
136
|
+
return f"❌ Download failed for {fmt}: {exc} (saved so far: {saved})"
|
|
137
|
+
out = {
|
|
138
|
+
"saved": saved,
|
|
139
|
+
"not_available": missing,
|
|
140
|
+
"license": part.get("license") if isinstance(part, dict) else None,
|
|
141
|
+
}
|
|
142
|
+
return json.dumps(out, indent=2)
|