kcaa 0.2.2__tar.gz → 0.2.3__tar.gz

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 (94) hide show
  1. {kcaa-0.2.2 → kcaa-0.2.3}/.gitignore +1 -0
  2. {kcaa-0.2.2 → kcaa-0.2.3}/PKG-INFO +8 -5
  3. {kcaa-0.2.2 → kcaa-0.2.3}/README.md +7 -4
  4. {kcaa-0.2.2 → kcaa-0.2.3}/README_CN.md +7 -4
  5. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/server.py +14 -8
  6. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/pcb_edit_tools.py +99 -26
  7. kcaa-0.2.3/kcaa/tools/pcb_library_tools.py +907 -0
  8. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/pcb_placement_tools.py +139 -72
  9. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/pcb_query_tools.py +152 -93
  10. kcaa-0.2.3/kcaa/tools/project_tools.py +132 -0
  11. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/sheet_tools.py +33 -17
  12. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/symbol_edit_tools.py +159 -88
  13. kcaa-0.2.3/kcaa/tools/version_tools.py +139 -0
  14. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/wire_edit_tools.py +79 -2
  15. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/footprint_database.py +198 -59
  16. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/footprint_index_manager.py +127 -23
  17. kcaa-0.2.3/kcaa/utils/fp_lib_table_utils.py +137 -0
  18. kcaa-0.2.3/kcaa/utils/pcb_footprint_utils.py +639 -0
  19. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/pcb_library_utils.py +1 -0
  20. kcaa-0.2.3/kcaa/utils/version_manager.py +250 -0
  21. {kcaa-0.2.2 → kcaa-0.2.3}/pyproject.toml +1 -1
  22. kcaa-0.2.2/kcaa/tools/pcb_library_tools.py +0 -422
  23. kcaa-0.2.2/kcaa/tools/project_tools.py +0 -62
  24. kcaa-0.2.2/kcaa/tools/version_tools.py +0 -134
  25. kcaa-0.2.2/kcaa/utils/pcb_footprint_utils.py +0 -240
  26. kcaa-0.2.2/kcaa/utils/version_manager.py +0 -143
  27. {kcaa-0.2.2 → kcaa-0.2.3}/LICENSE +0 -0
  28. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/__init__.py +0 -0
  29. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/context.py +0 -0
  30. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/prompts/__init__.py +0 -0
  31. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/prompts/bom_prompts.py +0 -0
  32. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/prompts/drc_prompt.py +0 -0
  33. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/prompts/pattern_prompts.py +0 -0
  34. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/prompts/templates.py +0 -0
  35. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/resources/__init__.py +0 -0
  36. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/resources/bom_resources.py +0 -0
  37. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/resources/drc_resources.py +0 -0
  38. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/resources/files.py +0 -0
  39. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/resources/netlist_resources.py +0 -0
  40. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/resources/pattern_resources.py +0 -0
  41. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/resources/projects.py +0 -0
  42. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/router/__init__.py +0 -0
  43. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/router/a_star.py +0 -0
  44. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/router/grid_a_star.py +0 -0
  45. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/router/path_postprocess.py +0 -0
  46. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/router/router.py +0 -0
  47. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/router/via_check.py +0 -0
  48. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/router/visibility_graph.py +0 -0
  49. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/router/world_model.py +0 -0
  50. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/__init__.py +0 -0
  51. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/analysis_tools.py +0 -0
  52. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/bom_tools.py +0 -0
  53. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/drc_tools.py +0 -0
  54. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/export_tools.py +0 -0
  55. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/kipy_tools.py +0 -0
  56. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/netlist_tools.py +0 -0
  57. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/pattern_tools.py +0 -0
  58. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/pcb_group_tools.py +0 -0
  59. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/pcb_placement_helpers.py +0 -0
  60. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/pcb_routing_tools.py +0 -0
  61. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/pcb_zone_tools.py +0 -0
  62. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/placement_helpers.py +0 -0
  63. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/schematic_group_tools.py +0 -0
  64. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/skill_tools.py +0 -0
  65. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/symbol_tools.py +0 -0
  66. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/tools/validation_tools.py +0 -0
  67. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/__init__.py +0 -0
  68. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/boundary_validator.py +0 -0
  69. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/component_utils.py +0 -0
  70. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/config.py +0 -0
  71. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/file_utils.py +0 -0
  72. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/ipc_drc.py +0 -0
  73. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/kicad_api_detection.py +0 -0
  74. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/kicad_cli.py +0 -0
  75. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/kicad_utils.py +0 -0
  76. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/kipy_reload.py +0 -0
  77. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/net_settings.py +0 -0
  78. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/netlist_parser.py +0 -0
  79. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/path_validator.py +0 -0
  80. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/pattern_recognition.py +0 -0
  81. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/pcb_board_utils.py +0 -0
  82. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/pcb_design_rules.py +0 -0
  83. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/pcb_sexp_utils.py +0 -0
  84. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/schematic_sexp_utils.py +0 -0
  85. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/secure_subprocess.py +0 -0
  86. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/skip_compat.py +0 -0
  87. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/skip_helpers.py +0 -0
  88. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/symbol_database.py +0 -0
  89. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/symbol_extractor.py +0 -0
  90. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/symbol_geometry.py +0 -0
  91. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/symbol_index_manager.py +0 -0
  92. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/symbol_index_reader.py +0 -0
  93. {kcaa-0.2.2 → kcaa-0.2.3}/kcaa/utils/temp_dir_manager.py +0 -0
  94. {kcaa-0.2.2 → kcaa-0.2.3}/kicad_plugin/README.md +0 -0
@@ -73,6 +73,7 @@ fp-info-cache
73
73
  *.kicad_pcb-bak
74
74
  *.kicad_sch-bak
75
75
  *.kicad_pro-bak
76
+ **/.versions/
76
77
  *.kicad_prl
77
78
  *.kicad_prl-bak
78
79
  *.kicad_sch.lck
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: kcaa
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Model Context Protocol (MCP) server for KiCad electronic design automation (EDA) files
5
5
  Project-URL: Homepage, https://github.com/paul356/KiCad-AI-Assistant
6
6
  Project-URL: Bug Tracker, https://github.com/paul356/KiCad-AI-Assistant/issues
@@ -292,6 +292,9 @@ kcaa
292
292
  | `list_footprint_libraries` | List all available footprint libraries |
293
293
  | `search_footprints` | Search footprints by name, description, or tag |
294
294
  | `get_footprint_details` | Get footprint details (pads, bounding box, etc.) |
295
+ | `find_footprints_not_in_libraries` | List board footprints missing from every indexed library |
296
+ | `create_footprint_library` | Create a global 3rdparty or project-local footprint library |
297
+ | `add_footprints_to_library` | Export explicitly named board footprints into a library |
295
298
 
296
299
  ### PCB Query
297
300
 
@@ -323,7 +326,7 @@ kcaa
323
326
 
324
327
  | Tool | Description |
325
328
  |------|-------------|
326
- | `set_footprint_position` | Move and/or rotate a single footprint |
329
+ | `set_footprint_position` | Move and/or rotate one or more footprints (via `items`) |
327
330
  | `flip_footprint` | Flip a footprint between top and bottom layer |
328
331
  | `align_footprints` | Align footprints to the same axis |
329
332
  | `distribute_footprints` | Distribute footprints evenly along an axis |
@@ -371,9 +374,9 @@ kcaa
371
374
 
372
375
  | Tool | Description |
373
376
  |------|-------------|
374
- | `save_file_version` | Save a version snapshot for rollback |
375
- | `list_file_versions` | List saved version snapshots |
376
- | `restore_file_version` | Restore to a previously saved version |
377
+ | `save_project_version` | Save a project version archive (schematic+PCB+project file) |
378
+ | `list_project_versions` | List saved project version archives |
379
+ | `restore_project_version` | Restore project files together to a saved archive |
377
380
  | `generate_pcb_thumbnail` | Render a PCB thumbnail image |
378
381
  | `generate_project_thumbnail` | Render a project thumbnail |
379
382
 
@@ -253,6 +253,9 @@ kcaa
253
253
  | `list_footprint_libraries` | List all available footprint libraries |
254
254
  | `search_footprints` | Search footprints by name, description, or tag |
255
255
  | `get_footprint_details` | Get footprint details (pads, bounding box, etc.) |
256
+ | `find_footprints_not_in_libraries` | List board footprints missing from every indexed library |
257
+ | `create_footprint_library` | Create a global 3rdparty or project-local footprint library |
258
+ | `add_footprints_to_library` | Export explicitly named board footprints into a library |
256
259
 
257
260
  ### PCB Query
258
261
 
@@ -284,7 +287,7 @@ kcaa
284
287
 
285
288
  | Tool | Description |
286
289
  |------|-------------|
287
- | `set_footprint_position` | Move and/or rotate a single footprint |
290
+ | `set_footprint_position` | Move and/or rotate one or more footprints (via `items`) |
288
291
  | `flip_footprint` | Flip a footprint between top and bottom layer |
289
292
  | `align_footprints` | Align footprints to the same axis |
290
293
  | `distribute_footprints` | Distribute footprints evenly along an axis |
@@ -332,9 +335,9 @@ kcaa
332
335
 
333
336
  | Tool | Description |
334
337
  |------|-------------|
335
- | `save_file_version` | Save a version snapshot for rollback |
336
- | `list_file_versions` | List saved version snapshots |
337
- | `restore_file_version` | Restore to a previously saved version |
338
+ | `save_project_version` | Save a project version archive (schematic+PCB+project file) |
339
+ | `list_project_versions` | List saved project version archives |
340
+ | `restore_project_version` | Restore project files together to a saved archive |
338
341
  | `generate_pcb_thumbnail` | Render a PCB thumbnail image |
339
342
  | `generate_project_thumbnail` | Render a project thumbnail |
340
343
 
@@ -252,6 +252,9 @@ kcaa
252
252
  | `list_footprint_libraries` | 列出所有可用封装库 |
253
253
  | `search_footprints` | 按名称/描述/标签搜索封装 |
254
254
  | `get_footprint_details` | 获取封装详情(焊盘、边界框等) |
255
+ | `find_footprints_not_in_libraries` | 列出板上缺失于所有已索引库的封装 |
256
+ | `create_footprint_library` | 创建全局 3rdparty 或项目级封装库 |
257
+ | `add_footprints_to_library` | 将显式指定的板载封装导出到库 |
255
258
 
256
259
  ### PCB 查询
257
260
 
@@ -283,7 +286,7 @@ kcaa
283
286
 
284
287
  | 工具 | 说明 |
285
288
  |------|------|
286
- | `set_footprint_position` | 移动/旋转单个封装 |
289
+ | `set_footprint_position` | 批量移动/旋转一个或多个封装 |
287
290
  | `flip_footprint` | 将封装在顶层/底层之间翻转 |
288
291
  | `align_footprints` | 将封装对齐到同一坐标轴 |
289
292
  | `distribute_footprints` | 沿坐标轴等间距分布封装 |
@@ -331,9 +334,9 @@ kcaa
331
334
 
332
335
  | 工具 | 说明 |
333
336
  |------|------|
334
- | `save_file_version` | 保存版本快照以便回退 |
335
- | `list_file_versions` | 列出已保存的版本快照 |
336
- | `restore_file_version` | 恢复到之前保存的版本 |
337
+ | `save_project_version` | 保存项目版本归档(原理图+PCB+项目文件) |
338
+ | `list_project_versions` | 列出已保存的项目版本归档 |
339
+ | `restore_project_version` | 将项目文件一起恢复到已保存的归档 |
337
340
  | `generate_pcb_thumbnail` | 渲染 PCB 缩略图 |
338
341
  | `generate_project_thumbnail` | 渲染项目缩略图 |
339
342
 
@@ -6,11 +6,12 @@ Two server profiles are supported, selected via the KICAD_MCP_PROFILE environmen
6
6
  full (default) — registers all tools, resources, and prompts. Intended for
7
7
  standalone MCP clients such as Claude Desktop.
8
8
 
9
- plugin — registers only the skip-based schematic editing tools
10
- (netlist, symbol, component, wire/junction). No resources,
11
- no prompts, no kicad-cli-dependent tools. Intended for the
12
- KiCad plugin integration where the plugin supplies project
13
- context directly and the LLM drives editing via tool calls.
9
+ plugin — registers the skip-based schematic/PCB editing tools
10
+ plus the ``get_project_structure`` query tool. No
11
+ resources, no prompts, no kicad-cli-dependent tools.
12
+ Intended for the KiCad plugin integration where the
13
+ plugin supplies project context directly and the LLM
14
+ drives editing via tool calls.
14
15
  """
15
16
 
16
17
  import atexit
@@ -40,6 +41,7 @@ from kcaa.tools.pcb_query_tools import register_pcb_query_tools
40
41
  from kcaa.tools.pcb_routing_tools import register_pcb_routing_tools
41
42
  from kcaa.tools.pcb_zone_tools import register_pcb_zone_tools
42
43
  from kcaa.tools.placement_helpers import register_placement_helpers
44
+ from kcaa.tools.project_tools import register_project_tools
43
45
  from kcaa.tools.schematic_group_tools import register_schematic_group_tools
44
46
  from kcaa.tools.sheet_tools import register_sheet_tools
45
47
  from kcaa.tools.skill_tools import register_skill_tools
@@ -110,9 +112,12 @@ def register_signal_handlers(server: FastMCP) -> None:
110
112
 
111
113
 
112
114
  def _register_plugin_profile(mcp: FastMCP) -> None:
113
- """Register only the skip-based schematic/PCB editing tools + IPC-based tools for the plugin profile."""
115
+ """Register the skip-based schematic/PCB editing tools + IPC-based tools for the plugin profile."""
114
116
  logging.info("Registering plugin profile: schematic + PCB tools")
115
117
  register_netlist_tools(mcp)
118
+ # Project structure query for the plugin LLM; keep full-profile
119
+ # management tools (list_projects/open_project) out of this profile.
120
+ register_project_tools(mcp, tools=("get_project_structure",))
116
121
  register_symbol_tools(mcp)
117
122
  register_symbol_edit_tools(mcp)
118
123
  register_sheet_tools(mcp)
@@ -205,8 +210,9 @@ def create_server(profile: ServerProfile = "full") -> FastMCP:
205
210
  Args:
206
211
  profile: Server capability profile.
207
212
  "full" — all tools, resources, and prompts (default; for Claude Desktop etc.)
208
- "plugin" — plugin-facing profile: 26 skip-based schematic editing tools only,
209
- no resources, no prompts, no kicad-cli-dependent tools.
213
+ "plugin" — plugin-facing profile: skip-based schematic/PCB editing
214
+ tools plus the get_project_structure query tool; no
215
+ resources, no prompts, no kicad-cli-dependent tools.
210
216
  Override via KICAD_MCP_PROFILE env var.
211
217
  """
212
218
  # Allow env var to override the profile argument; strip whitespace/case variants
@@ -309,47 +309,120 @@ def register_pcb_edit_tools(mcp: FastMCP) -> None:
309
309
  @mcp.tool()
310
310
  async def set_footprint_property(
311
311
  pcb_path: str,
312
- reference: str,
313
- property_name: str,
314
- value: str,
312
+ items: list[dict[str, Any]],
315
313
  ctx: Context | None,
316
- ) -> dict[str, str]:
317
- """Update a property of a footprint on the PCB board.
314
+ ) -> dict[str, Any]:
315
+ """Update a property on footprints of the PCB board.
318
316
 
319
317
  Common property names: ``Reference``, ``Value``, ``Datasheet``,
320
318
  ``Description``. Custom user fields are also supported.
321
319
 
320
+ Each item in *items* carries its own property and value
321
+ (``{"reference": ..., "property_name": ..., "value": ...}``).
322
+ All items are processed in one parse + one save (single ``.bak``).
323
+ Partial-apply: a footprint that cannot be found or lacks the
324
+ property keeps its own error in ``results`` while the remaining
325
+ footprints are still applied and saved. Empty, duplicate, or
326
+ malformed items are rejected up front.
327
+
322
328
  A .kicad_pcb.bak backup is created before writing.
329
+
330
+ Args:
331
+ pcb_path: Absolute path to the .kicad_pcb file.
332
+ items: List of per-footprint specs, e.g. ``[{"reference":
333
+ "R1", "property_name": "Value", "value": "22k"}]``.
334
+ Keys: ``reference`` (str, required, unique),
335
+ ``property_name`` (str, required),
336
+ ``value`` (str, required).
337
+
338
+ Returns:
339
+ dict with: results (per-footprint {reference, property_name,
340
+ previous_value, new_value} or {reference, error}),
341
+ success (all applied), count, applied_count, failure_count,
342
+ backup_path, pcb_path.
323
343
  """
344
+ _ITEM_KEYS = {"reference", "property_name", "value"}
345
+ if not items:
346
+ return {"error": "items must not be empty"}
347
+ for item in items:
348
+ if not isinstance(item, dict):
349
+ return {"error": "each item must be a dict with reference, property_name, value"}
350
+ unknown = set(item) - _ITEM_KEYS
351
+ if unknown:
352
+ return {
353
+ "error": f"Unknown item fields: {sorted(unknown)}. Valid: {sorted(_ITEM_KEYS)}"
354
+ }
355
+ ref = item.get("reference")
356
+ if not isinstance(ref, str) or not ref:
357
+ return {"error": "items must not contain empty or missing references"}
358
+ if not isinstance(item.get("property_name"), str) or not item["property_name"]:
359
+ return {"error": "items must contain a non-empty property_name"}
360
+ if not isinstance(item.get("value"), str):
361
+ return {"error": "items must contain a string value"}
362
+ refs = [item["reference"] for item in items]
363
+ if len(set(refs)) != len(refs):
364
+ return {"error": "items must not contain duplicate references"}
365
+
324
366
  data = load_pcb(pcb_path)
325
- try:
326
- fp = find_footprint(data, reference)
327
- except KeyError as exc:
328
- return {"error": str(exc)}
329
367
 
330
- old_value = get_fp_property(fp, property_name)
331
- if old_value is None:
368
+ def apply_one(item: dict[str, Any]) -> dict[str, Any]:
369
+ ref = item["reference"]
370
+ name = item["property_name"]
371
+ val = item["value"]
372
+ try:
373
+ fp = find_footprint(data, ref)
374
+ except KeyError as exc:
375
+ return {"error": str(exc), "reference": ref}
376
+
377
+ old_value = get_fp_property(fp, name)
378
+ if old_value is None:
379
+ return {
380
+ "error": (
381
+ f"Property '{name}' not found on footprint '{ref}'. "
382
+ f"Use get_footprint to see available properties."
383
+ ),
384
+ "reference": ref,
385
+ }
386
+
387
+ updated = set_fp_property(fp, name, val)
388
+ if not updated:
389
+ return {
390
+ "error": f"Failed to update property '{name}' on '{ref}'.",
391
+ "reference": ref,
392
+ }
393
+
332
394
  return {
333
- "error": (
334
- f"Property '{property_name}' not found on footprint '{reference}'. "
335
- f"Use get_footprint to see available properties."
336
- )
395
+ "success": True,
396
+ "reference": ref,
397
+ "property_name": name,
398
+ "previous_value": old_value,
399
+ "new_value": val,
337
400
  }
338
401
 
339
- updated = set_fp_property(fp, property_name, value)
340
- if not updated:
341
- return {"error": f"Failed to update property '{property_name}' on '{reference}'."}
402
+ results: list[dict[str, Any]] = []
403
+ for item in items:
404
+ ref = item["reference"]
405
+ try:
406
+ results.append(apply_one(item))
407
+ except Exception as exc:
408
+ log.warning("set_footprint_property: reference %r failed: %s", ref, exc)
409
+ results.append({"error": f"{ref}: {exc}", "reference": ref})
342
410
 
343
- try:
344
- backup_path = save_pcb(pcb_path, data)
345
- except OSError as exc:
346
- return {"error": f"Failed to write PCB file: {exc}"}
411
+ applied_count = sum(1 for r in results if r.get("success"))
412
+
413
+ backup_path: str | None = None
414
+ if applied_count > 0:
415
+ try:
416
+ backup_path = save_pcb(pcb_path, data)
417
+ except OSError as exc:
418
+ return {"error": f"Failed to write PCB file: {exc}"}
347
419
 
348
420
  return {
349
- "reference": reference,
350
- "property_name": property_name,
351
- "previous_value": old_value,
352
- "new_value": value,
421
+ "success": applied_count == len(results) and len(results) > 0,
422
+ "results": results,
423
+ "count": len(results),
424
+ "applied_count": applied_count,
425
+ "failure_count": len(results) - applied_count,
353
426
  "backup_path": backup_path,
354
427
  "pcb_path": pcb_path,
355
428
  }