kicad-sch-api 0.1.0__py3-none-any.whl → 0.1.2__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.

Potentially problematic release.


This version of kicad-sch-api might be problematic. Click here for more details.

kicad_sch_api/cli.py ADDED
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ KiCAD Schematic API - Command Line Interface
4
+
5
+ Provides helpful commands for setup, testing, and usage of the MCP server.
6
+ """
7
+
8
+ import sys
9
+ import json
10
+ import os
11
+ import subprocess
12
+ import argparse
13
+ from pathlib import Path
14
+ from typing import Dict, Any
15
+
16
+ def get_claude_config_path() -> Path:
17
+ """Get the Claude Code configuration file path for current platform."""
18
+ if sys.platform == "darwin":
19
+ return Path.home() / "Library/Application Support/Claude/claude_desktop_config.json"
20
+ elif sys.platform == "win32":
21
+ return Path(os.environ["APPDATA"]) / "Claude/claude_desktop_config.json"
22
+ else: # Linux and others
23
+ return Path.home() / ".config/Claude/claude_desktop_config.json"
24
+
25
+ def setup_claude_code() -> bool:
26
+ """Automatically configure Claude Code MCP settings."""
27
+ print("🔧 Setting up Claude Code MCP configuration...")
28
+
29
+ config_path = get_claude_config_path()
30
+
31
+ # Create directory if it doesn't exist
32
+ config_path.parent.mkdir(parents=True, exist_ok=True)
33
+
34
+ # Backup existing config
35
+ if config_path.exists():
36
+ backup_path = config_path.with_suffix(f".backup.{config_path.stat().st_mtime:.0f}.json")
37
+ backup_path.write_text(config_path.read_text())
38
+ print(f"📁 Backed up existing config to: {backup_path}")
39
+
40
+ # Determine MCP command path
41
+ mcp_command = os.environ.get('FOUND_MCP_PATH', 'kicad-sch-mcp')
42
+
43
+ # If still using default, try to find the actual path
44
+ if mcp_command == 'kicad-sch-mcp':
45
+ python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
46
+ possible_paths = [
47
+ f"/Library/Frameworks/Python.framework/Versions/{python_version}/bin/kicad-sch-mcp",
48
+ os.path.expanduser(f"~/Library/Python/{python_version}/bin/kicad-sch-mcp"),
49
+ os.path.expanduser("~/.local/bin/kicad-sch-mcp"),
50
+ "/usr/local/bin/kicad-sch-mcp"
51
+ ]
52
+
53
+ for path_to_try in possible_paths:
54
+ if os.path.exists(path_to_try) and os.access(path_to_try, os.X_OK):
55
+ mcp_command = path_to_try
56
+ print(f"📍 Using MCP command at: {mcp_command}")
57
+ break
58
+
59
+ # Create new configuration
60
+ config = {
61
+ "mcpServers": {
62
+ "kicad-sch-api": {
63
+ "command": mcp_command,
64
+ "args": [],
65
+ "env": {}
66
+ }
67
+ }
68
+ }
69
+
70
+ # Write configuration
71
+ with open(config_path, 'w') as f:
72
+ json.dump(config, f, indent=2)
73
+
74
+ print(f"✅ Configuration written to: {config_path}")
75
+ print("🔄 Please restart Claude Code to apply changes")
76
+ return True
77
+
78
+ def test_installation() -> bool:
79
+ """Test that the MCP server is working correctly."""
80
+ print("🧪 Testing KiCAD Schematic MCP Server...")
81
+
82
+ try:
83
+ # Test import
84
+ from kicad_sch_api.mcp.server import main as mcp_main
85
+ print("✅ MCP server module imports successfully")
86
+
87
+ # Test component discovery
88
+ from kicad_sch_api.discovery.search_index import get_search_index
89
+ print("✅ Component discovery system available")
90
+
91
+ # Test KiCAD library access
92
+ from kicad_sch_api.library.cache import get_symbol_cache
93
+ cache = get_symbol_cache()
94
+ stats = cache.get_performance_stats()
95
+ print(f"✅ Symbol cache initialized: {stats['total_symbols_cached']} symbols")
96
+
97
+ print("🎉 All tests passed!")
98
+ return True
99
+
100
+ except Exception as e:
101
+ print(f"❌ Test failed: {e}")
102
+ return False
103
+
104
+ def show_status() -> bool:
105
+ """Show current installation and configuration status."""
106
+ print("📊 KiCAD Schematic MCP Server Status")
107
+ print("=" * 40)
108
+
109
+ # Check installation
110
+ try:
111
+ import kicad_sch_api
112
+ version = getattr(kicad_sch_api, '__version__', 'unknown')
113
+ print(f"✅ Package installed: v{version}")
114
+ except ImportError:
115
+ print("❌ Package not installed")
116
+ return False
117
+
118
+ # Check MCP command
119
+ mcp_cmd_path = None
120
+ try:
121
+ result = subprocess.run(['kicad-sch-mcp', '--help'],
122
+ capture_output=True, timeout=5)
123
+ if result.returncode == 0:
124
+ print("✅ MCP command available")
125
+ mcp_cmd_path = "kicad-sch-mcp"
126
+ else:
127
+ print("⚠️ MCP command found but returns error")
128
+ except (subprocess.TimeoutExpired, FileNotFoundError):
129
+ # Try to find the command in common locations
130
+ import sys
131
+ python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
132
+ possible_paths = [
133
+ f"/Library/Frameworks/Python.framework/Versions/{python_version}/bin/kicad-sch-mcp",
134
+ os.path.expanduser(f"~/Library/Python/{python_version}/bin/kicad-sch-mcp"),
135
+ os.path.expanduser("~/.local/bin/kicad-sch-mcp"),
136
+ "/usr/local/bin/kicad-sch-mcp"
137
+ ]
138
+
139
+ for path_to_try in possible_paths:
140
+ if os.path.exists(path_to_try) and os.access(path_to_try, os.X_OK):
141
+ print(f"✅ MCP command found at: {path_to_try}")
142
+ print("⚠️ Note: Command not in PATH, but found at above location")
143
+ mcp_cmd_path = path_to_try
144
+ break
145
+
146
+ if not mcp_cmd_path:
147
+ print("❌ MCP command not found in PATH")
148
+ print(" You may need to add Python scripts directory to your PATH")
149
+ print(f" Try: export PATH=\"/Library/Frameworks/Python.framework/Versions/{python_version}/bin:$PATH\"")
150
+
151
+ # Store found path for potential use in configuration
152
+ if mcp_cmd_path:
153
+ os.environ['FOUND_MCP_PATH'] = mcp_cmd_path
154
+
155
+ # Check Claude Code configuration
156
+ config_path = get_claude_config_path()
157
+ if config_path.exists():
158
+ try:
159
+ with open(config_path) as f:
160
+ config = json.load(f)
161
+ if "kicad-sch-api" in config.get("mcpServers", {}):
162
+ print("✅ Claude Code MCP configuration found")
163
+ else:
164
+ print("⚠️ Claude Code config exists but kicad-sch-api not configured")
165
+ except json.JSONDecodeError:
166
+ print("❌ Claude Code config file is invalid JSON")
167
+ else:
168
+ print("❌ Claude Code configuration not found")
169
+
170
+ # Check KiCAD libraries
171
+ try:
172
+ from kicad_sch_api.library.cache import get_symbol_cache
173
+ cache = get_symbol_cache()
174
+ stats = cache.get_performance_stats()
175
+ print(f"✅ KiCAD libraries: {len(cache._lib_stats)} libraries, {stats['total_symbols_cached']} symbols")
176
+ except Exception as e:
177
+ print(f"⚠️ KiCAD library access: {e}")
178
+
179
+ return True
180
+
181
+ def create_demo() -> bool:
182
+ """Create a demo schematic to test functionality."""
183
+ print("🎨 Creating demo schematic...")
184
+
185
+ try:
186
+ import kicad_sch_api as ksa
187
+
188
+ # Create demo schematic
189
+ sch = ksa.create_schematic("Demo_Circuit")
190
+
191
+ # Add components
192
+ resistor = sch.components.add('Device:R', reference='R1', value='10k', position=(100, 100))
193
+ capacitor = sch.components.add('Device:C', reference='C1', value='100nF', position=(150, 100))
194
+ led = sch.components.add('Device:LED', reference='D1', value='LED', position=(200, 100))
195
+
196
+ # Add a hierarchical sheet
197
+ sheet_uuid = sch.add_sheet(
198
+ name="Subcircuit",
199
+ filename="subcircuit_demo.kicad_sch",
200
+ position=(100, 150),
201
+ size=(60, 40)
202
+ )
203
+
204
+ # Add sheet pins
205
+ sch.add_sheet_pin(sheet_uuid, "VCC", "input", (0, 10))
206
+ sch.add_sheet_pin(sheet_uuid, "GND", "input", (0, 30))
207
+
208
+ # Save schematic
209
+ sch.save("demo_circuit.kicad_sch")
210
+
211
+ print("✅ Demo schematic created: demo_circuit.kicad_sch")
212
+ print("📁 Contains: resistor, capacitor, LED, and hierarchical sheet")
213
+ print("🔗 Try opening in KiCAD: kicad demo_circuit.kicad_sch")
214
+
215
+ return True
216
+
217
+ except Exception as e:
218
+ print(f"❌ Demo creation failed: {e}")
219
+ return False
220
+
221
+ def init_cache() -> bool:
222
+ """Initialize the component discovery cache."""
223
+ print("🔄 Initializing component discovery cache...")
224
+
225
+ try:
226
+ from kicad_sch_api.discovery.search_index import ensure_index_built
227
+ component_count = ensure_index_built()
228
+ print(f"✅ Component cache initialized: {component_count} components indexed")
229
+ return True
230
+ except Exception as e:
231
+ print(f"❌ Cache initialization failed: {e}")
232
+ return False
233
+
234
+ def check_kicad() -> bool:
235
+ """Check KiCAD installation and library access."""
236
+ print("🔍 Checking KiCAD installation...")
237
+
238
+ try:
239
+ # Check if KiCAD command is available
240
+ result = subprocess.run(['kicad', '--version'],
241
+ capture_output=True, timeout=10)
242
+ if result.returncode == 0:
243
+ version_output = result.stdout.decode().strip()
244
+ print(f"✅ KiCAD found: {version_output}")
245
+ else:
246
+ print("⚠️ KiCAD command found but version check failed")
247
+ except (subprocess.TimeoutExpired, FileNotFoundError):
248
+ print("❌ KiCAD command not found in PATH")
249
+ print(" Please ensure KiCAD is installed and accessible")
250
+
251
+ # Check library directories
252
+ try:
253
+ from kicad_sch_api.library.cache import get_symbol_cache
254
+ cache = get_symbol_cache()
255
+
256
+ print("📚 KiCAD Library Status:")
257
+ for lib_name, lib_stats in cache._lib_stats.items():
258
+ print(f" • {lib_name}: {lib_stats.symbol_count} symbols")
259
+
260
+ return True
261
+ except Exception as e:
262
+ print(f"❌ Library access failed: {e}")
263
+ return False
264
+
265
+ def show_logs():
266
+ """Show recent MCP server logs."""
267
+ print("📜 Recent MCP Server Logs")
268
+ print("=" * 25)
269
+
270
+ # For now, just run the server with debug output
271
+ print("To view live logs, run:")
272
+ print(" kicad-sch-mcp --debug")
273
+ print()
274
+ print("Or check Claude Code logs in:")
275
+ if sys.platform == "darwin":
276
+ print(" ~/Library/Logs/Claude/mcp-server-kicad-sch-api.log")
277
+ elif sys.platform == "win32":
278
+ print(" %USERPROFILE%\\AppData\\Local\\Claude\\Logs\\mcp-server-kicad-sch-api.log")
279
+ else:
280
+ print(" ~/.local/share/Claude/logs/mcp-server-kicad-sch-api.log")
281
+
282
+ def main():
283
+ """Main CLI entry point."""
284
+ parser = argparse.ArgumentParser(
285
+ description="KiCAD Schematic API - Command Line Interface",
286
+ formatter_class=argparse.RawDescriptionHelpFormatter,
287
+ epilog="""
288
+ Examples:
289
+ kicad-sch-api --setup-claude-code # Configure Claude Code MCP
290
+ kicad-sch-api --test # Test installation
291
+ kicad-sch-api --demo # Create demo schematic
292
+ kicad-sch-api --status # Show status
293
+ """
294
+ )
295
+
296
+ parser.add_argument('--setup-claude-code', action='store_true',
297
+ help='Configure Claude Code MCP settings automatically')
298
+ parser.add_argument('--test', action='store_true',
299
+ help='Test that the installation is working')
300
+ parser.add_argument('--status', action='store_true',
301
+ help='Show installation and configuration status')
302
+ parser.add_argument('--demo', action='store_true',
303
+ help='Create a demo schematic')
304
+ parser.add_argument('--init-cache', action='store_true',
305
+ help='Initialize component discovery cache')
306
+ parser.add_argument('--check-kicad', action='store_true',
307
+ help='Check KiCAD installation and libraries')
308
+ parser.add_argument('--logs', action='store_true',
309
+ help='Show recent MCP server logs')
310
+
311
+ args = parser.parse_args()
312
+
313
+ # If no arguments provided, show help
314
+ if not any(vars(args).values()):
315
+ parser.print_help()
316
+ return
317
+
318
+ # Execute requested actions
319
+ success = True
320
+
321
+ if args.setup_claude_code:
322
+ success &= setup_claude_code()
323
+
324
+ if args.test:
325
+ success &= test_installation()
326
+
327
+ if args.status:
328
+ success &= show_status()
329
+
330
+ if args.demo:
331
+ success &= create_demo()
332
+
333
+ if args.init_cache:
334
+ success &= init_cache()
335
+
336
+ if args.check_kicad:
337
+ success &= check_kicad()
338
+
339
+ if args.logs:
340
+ show_logs()
341
+
342
+ sys.exit(0 if success else 1)
343
+
344
+ if __name__ == "__main__":
345
+ main()
@@ -51,13 +51,15 @@ class ExactFormatter:
51
51
  self.rules["kicad_sch"] = FormatRule(inline=False, indent_level=0)
52
52
  self.rules["version"] = FormatRule(inline=True)
53
53
  self.rules["generator"] = FormatRule(inline=True, quote_indices={1})
54
+ self.rules["generator_version"] = FormatRule(inline=True, quote_indices={1})
54
55
  self.rules["uuid"] = FormatRule(inline=True, quote_indices={1})
56
+ self.rules["paper"] = FormatRule(inline=True, quote_indices={1})
55
57
 
56
58
  # Title block
57
59
  self.rules["title_block"] = FormatRule(inline=False)
58
60
  self.rules["title"] = FormatRule(inline=True, quote_indices={1})
59
61
  self.rules["company"] = FormatRule(inline=True, quote_indices={1})
60
- self.rules["revision"] = FormatRule(inline=True, quote_indices={1})
62
+ self.rules["rev"] = FormatRule(inline=True, quote_indices={1}) # KiCAD uses "rev"
61
63
  self.rules["date"] = FormatRule(inline=True, quote_indices={1})
62
64
  self.rules["size"] = FormatRule(inline=True, quote_indices={1})
63
65
  self.rules["comment"] = FormatRule(inline=True, quote_indices={2})
@@ -259,9 +259,10 @@ class SExpressionParser:
259
259
  if schematic_data.get("paper"):
260
260
  sexp_data.append([sexpdata.Symbol("paper"), schematic_data["paper"]])
261
261
 
262
- # Add title block
263
- if schematic_data.get("title_block"):
264
- sexp_data.append(self._title_block_to_sexp(schematic_data["title_block"]))
262
+ # Add title block only if it has non-default content
263
+ title_block = schematic_data.get("title_block")
264
+ if title_block and any(title_block.get(key) for key in ["title", "company", "revision", "date", "comments"]):
265
+ sexp_data.append(self._title_block_to_sexp(title_block))
265
266
 
266
267
  # Add lib_symbols (always include for KiCAD compatibility)
267
268
  lib_symbols = schematic_data.get("lib_symbols", {})
@@ -421,8 +422,18 @@ class SExpressionParser:
421
422
  def _title_block_to_sexp(self, title_block: Dict[str, Any]) -> List[Any]:
422
423
  """Convert title block to S-expression."""
423
424
  sexp = [sexpdata.Symbol("title_block")]
424
- for key, value in title_block.items():
425
- sexp.append([sexpdata.Symbol(key), value])
425
+
426
+ # Add standard fields
427
+ for key in ["title", "date", "rev", "company"]:
428
+ if key in title_block and title_block[key]:
429
+ sexp.append([sexpdata.Symbol(key), title_block[key]])
430
+
431
+ # Add comments with special formatting
432
+ comments = title_block.get("comments", {})
433
+ if isinstance(comments, dict):
434
+ for comment_num, comment_text in comments.items():
435
+ sexp.append([sexpdata.Symbol("comment"), comment_num, comment_text])
436
+
426
437
  return sexp
427
438
 
428
439
  def _symbol_to_sexp(self, symbol_data: Dict[str, Any], schematic_uuid: str = None) -> List[Any]:
@@ -177,7 +177,9 @@ class Schematic:
177
177
  schematic_data["paper"] = paper
178
178
  if uuid:
179
179
  schematic_data["uuid"] = uuid
180
- schematic_data["title_block"] = {"title": name}
180
+ # Only add title_block for non-default names to match reference format
181
+ if name and name not in ["Untitled", "Blank Schematic", "Single Resistor", "Two Resistors"]:
182
+ schematic_data["title_block"] = {"title": name}
181
183
 
182
184
  logger.info(f"Created new schematic: {name}")
183
185
  return cls(schematic_data)
@@ -813,6 +815,38 @@ class Schematic:
813
815
  logger.debug(f"Added text box: '{text}' at {position} size {size}")
814
816
  return text_box.uuid
815
817
 
818
+ def set_title_block(
819
+ self,
820
+ title: str = "",
821
+ date: str = "",
822
+ rev: str = "",
823
+ company: str = "",
824
+ comments: Optional[Dict[int, str]] = None
825
+ ):
826
+ """
827
+ Set title block information.
828
+
829
+ Args:
830
+ title: Schematic title
831
+ date: Creation/revision date
832
+ rev: Revision number
833
+ company: Company name
834
+ comments: Numbered comments (1, 2, 3, etc.)
835
+ """
836
+ if comments is None:
837
+ comments = {}
838
+
839
+ self._data["title_block"] = {
840
+ "title": title,
841
+ "date": date,
842
+ "rev": rev,
843
+ "company": company,
844
+ "comments": comments
845
+ }
846
+ self._modified = True
847
+
848
+ logger.debug(f"Set title block: {title} rev {rev}")
849
+
816
850
  # Library management
817
851
  @property
818
852
  def libraries(self) -> "LibraryManager":
@@ -1121,13 +1155,6 @@ class Schematic:
1121
1155
  "generator_version": "9.0",
1122
1156
  "uuid": str(uuid.uuid4()),
1123
1157
  "paper": "A4",
1124
- "title_block": {
1125
- "title": "Untitled",
1126
- "date": "",
1127
- "revision": "1.0",
1128
- "company": "",
1129
- "size": "A4",
1130
- },
1131
1158
  "components": [],
1132
1159
  "wires": [],
1133
1160
  "junctions": [],
@@ -409,7 +409,7 @@ class TitleBlock:
409
409
 
410
410
  title: str = ""
411
411
  company: str = ""
412
- revision: str = ""
412
+ rev: str = "" # KiCAD uses "rev" not "revision"
413
413
  date: str = ""
414
414
  size: str = "A4"
415
415
  comments: Dict[int, str] = field(default_factory=dict)
@@ -0,0 +1,10 @@
1
+ """
2
+ Component discovery tools for KiCAD schematic API.
3
+
4
+ This module provides fast search and discovery capabilities for KiCAD components
5
+ using a SQLite search index built from the existing symbol cache.
6
+ """
7
+
8
+ from .search_index import ComponentSearchIndex, get_search_index
9
+
10
+ __all__ = ["ComponentSearchIndex", "get_search_index"]