mfcli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. mfcli/.env.example +72 -0
  2. mfcli/__init__.py +0 -0
  3. mfcli/agents/__init__.py +0 -0
  4. mfcli/agents/controller/__init__.py +0 -0
  5. mfcli/agents/controller/agent.py +19 -0
  6. mfcli/agents/controller/config.yaml +27 -0
  7. mfcli/agents/controller/tools.py +42 -0
  8. mfcli/agents/tools/general.py +118 -0
  9. mfcli/alembic/env.py +61 -0
  10. mfcli/alembic/script.py.mako +28 -0
  11. mfcli/alembic/versions/6ccc0c7c397c_added_fields_to_pdf_parts_model.py +39 -0
  12. mfcli/alembic/versions/769019ef4870_added_gemini_file_path_to_pdf_part_model.py +33 -0
  13. mfcli/alembic/versions/7a2e3a779fdc_added_functional_block_and_component_.py +54 -0
  14. mfcli/alembic/versions/7d5adb2a47a7_added_pdf_parts_model.py +41 -0
  15. mfcli/alembic/versions/7fcb7d6a5836_init.py +167 -0
  16. mfcli/alembic/versions/e0f2b5765c72_added_cascade_delete_for_models_that_.py +32 -0
  17. mfcli/alembic.ini +147 -0
  18. mfcli/cli/__init__.py +0 -0
  19. mfcli/cli/dependencies.py +59 -0
  20. mfcli/cli/main.py +192 -0
  21. mfcli/client/__init__.py +0 -0
  22. mfcli/client/chroma_db.py +184 -0
  23. mfcli/client/docling.py +44 -0
  24. mfcli/client/gemini.py +252 -0
  25. mfcli/client/llama_parse.py +38 -0
  26. mfcli/client/vector_db.py +93 -0
  27. mfcli/constants/__init__.py +0 -0
  28. mfcli/constants/base_enum.py +18 -0
  29. mfcli/constants/directory_names.py +1 -0
  30. mfcli/constants/file_types.py +189 -0
  31. mfcli/constants/gemini.py +1 -0
  32. mfcli/constants/openai.py +6 -0
  33. mfcli/constants/pipeline_run_status.py +3 -0
  34. mfcli/crud/__init__.py +0 -0
  35. mfcli/crud/file.py +42 -0
  36. mfcli/crud/functional_blocks.py +26 -0
  37. mfcli/crud/netlist.py +18 -0
  38. mfcli/crud/pipeline_run.py +17 -0
  39. mfcli/crud/project.py +99 -0
  40. mfcli/digikey/__init__.py +0 -0
  41. mfcli/digikey/digikey.py +105 -0
  42. mfcli/main.py +5 -0
  43. mfcli/mcp/__init__.py +0 -0
  44. mfcli/mcp/configs/cline_mcp_settings.json +11 -0
  45. mfcli/mcp/configs/mfcli.mcp.json +7 -0
  46. mfcli/mcp/mcp_instance.py +6 -0
  47. mfcli/mcp/server.py +37 -0
  48. mfcli/mcp/state_manager.py +51 -0
  49. mfcli/mcp/tools/__init__.py +0 -0
  50. mfcli/mcp/tools/query_knowledgebase.py +108 -0
  51. mfcli/models/__init__.py +10 -0
  52. mfcli/models/base.py +10 -0
  53. mfcli/models/bom.py +71 -0
  54. mfcli/models/datasheet.py +10 -0
  55. mfcli/models/debug_setup.py +64 -0
  56. mfcli/models/file.py +43 -0
  57. mfcli/models/file_docket.py +94 -0
  58. mfcli/models/file_metadata.py +19 -0
  59. mfcli/models/functional_blocks.py +94 -0
  60. mfcli/models/llm_response.py +5 -0
  61. mfcli/models/mcu.py +97 -0
  62. mfcli/models/mcu_errata.py +26 -0
  63. mfcli/models/netlist.py +59 -0
  64. mfcli/models/pdf_parts.py +25 -0
  65. mfcli/models/pipeline_run.py +34 -0
  66. mfcli/models/project.py +27 -0
  67. mfcli/models/project_metadata.py +15 -0
  68. mfcli/pipeline/__init__.py +0 -0
  69. mfcli/pipeline/analysis/__init__.py +0 -0
  70. mfcli/pipeline/analysis/bom_netlist_mapper.py +28 -0
  71. mfcli/pipeline/analysis/generators/__init__.py +0 -0
  72. mfcli/pipeline/analysis/generators/bom/__init__.py +0 -0
  73. mfcli/pipeline/analysis/generators/bom/bom.py +74 -0
  74. mfcli/pipeline/analysis/generators/debug_setup/__init__.py +0 -0
  75. mfcli/pipeline/analysis/generators/debug_setup/debug_setup.py +71 -0
  76. mfcli/pipeline/analysis/generators/debug_setup/instructions.py +150 -0
  77. mfcli/pipeline/analysis/generators/functional_blocks/__init__.py +0 -0
  78. mfcli/pipeline/analysis/generators/functional_blocks/functional_blocks.py +93 -0
  79. mfcli/pipeline/analysis/generators/functional_blocks/instructions.py +34 -0
  80. mfcli/pipeline/analysis/generators/functional_blocks/validator.py +94 -0
  81. mfcli/pipeline/analysis/generators/generator.py +258 -0
  82. mfcli/pipeline/analysis/generators/generator_base.py +18 -0
  83. mfcli/pipeline/analysis/generators/mcu/__init__.py +0 -0
  84. mfcli/pipeline/analysis/generators/mcu/instructions.py +156 -0
  85. mfcli/pipeline/analysis/generators/mcu/mcu.py +84 -0
  86. mfcli/pipeline/analysis/generators/mcu_errata/__init__.py +1 -0
  87. mfcli/pipeline/analysis/generators/mcu_errata/instructions.py +77 -0
  88. mfcli/pipeline/analysis/generators/mcu_errata/mcu_errata.py +95 -0
  89. mfcli/pipeline/analysis/generators/summary/__init__.py +0 -0
  90. mfcli/pipeline/analysis/generators/summary/summary.py +47 -0
  91. mfcli/pipeline/classifier.py +93 -0
  92. mfcli/pipeline/data_enricher.py +15 -0
  93. mfcli/pipeline/extractor.py +34 -0
  94. mfcli/pipeline/extractors/__init__.py +0 -0
  95. mfcli/pipeline/extractors/pdf.py +12 -0
  96. mfcli/pipeline/parser.py +120 -0
  97. mfcli/pipeline/parsers/__init__.py +0 -0
  98. mfcli/pipeline/parsers/netlist/__init__.py +0 -0
  99. mfcli/pipeline/parsers/netlist/edif.py +93 -0
  100. mfcli/pipeline/parsers/netlist/kicad_legacy_net.py +326 -0
  101. mfcli/pipeline/parsers/netlist/kicad_spice.py +135 -0
  102. mfcli/pipeline/parsers/netlist/pads.py +185 -0
  103. mfcli/pipeline/parsers/netlist/protel.py +166 -0
  104. mfcli/pipeline/parsers/netlist/protel_detector.py +29 -0
  105. mfcli/pipeline/pipeline.py +419 -0
  106. mfcli/pipeline/preprocessors/__init__.py +0 -0
  107. mfcli/pipeline/preprocessors/user_guide.py +127 -0
  108. mfcli/pipeline/run_context.py +32 -0
  109. mfcli/pipeline/schema_mapper.py +89 -0
  110. mfcli/pipeline/sub_classifier.py +115 -0
  111. mfcli/utils/__init__.py +0 -0
  112. mfcli/utils/config.py +33 -0
  113. mfcli/utils/configurator.py +324 -0
  114. mfcli/utils/data_cleaner.py +82 -0
  115. mfcli/utils/datasheet_vectorizer.py +281 -0
  116. mfcli/utils/directory_manager.py +96 -0
  117. mfcli/utils/file_upload.py +298 -0
  118. mfcli/utils/files.py +16 -0
  119. mfcli/utils/http_requests.py +54 -0
  120. mfcli/utils/kb_lister.py +89 -0
  121. mfcli/utils/kb_remover.py +173 -0
  122. mfcli/utils/logger.py +28 -0
  123. mfcli/utils/mcp_configurator.py +311 -0
  124. mfcli/utils/migrations.py +18 -0
  125. mfcli/utils/orm.py +43 -0
  126. mfcli/utils/pdf_splitter.py +63 -0
  127. mfcli/utils/query_service.py +22 -0
  128. mfcli/utils/system_check.py +306 -0
  129. mfcli/utils/tools.py +31 -0
  130. mfcli/utils/vectorizer.py +28 -0
  131. mfcli-0.2.0.dist-info/METADATA +841 -0
  132. mfcli-0.2.0.dist-info/RECORD +136 -0
  133. mfcli-0.2.0.dist-info/WHEEL +5 -0
  134. mfcli-0.2.0.dist-info/entry_points.txt +3 -0
  135. mfcli-0.2.0.dist-info/licenses/LICENSE +21 -0
  136. mfcli-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,173 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import List
4
+ from mfcli.client.chroma_db import get_chromadb_client_for_project_name
5
+ from mfcli.models.file_docket import FileDocket
6
+ from mfcli.utils.directory_manager import app_dirs
7
+ from mfcli.utils.logger import get_logger
8
+ from mfcli.utils.orm import Session
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ def _remove_from_file_docket(matching_files: set) -> int:
14
+ """
15
+ Remove files from the file_docket.json based on file names.
16
+
17
+ Args:
18
+ matching_files: Set of file names to remove
19
+
20
+ Returns:
21
+ Number of entries removed from docket
22
+ """
23
+ if not app_dirs.file_docket_path or not app_dirs.file_docket_path.exists():
24
+ logger.warning("file_docket.json not found, skipping docket removal")
25
+ return 0
26
+
27
+ try:
28
+ # Load existing file docket
29
+ docket = FileDocket()
30
+ docket.load_from_json(app_dirs.file_docket_path)
31
+
32
+ # Track how many entries we remove
33
+ removed_count = 0
34
+
35
+ # Find and remove matching entries
36
+ entries_to_remove = []
37
+ for entry in docket._docket.entries:
38
+ if entry.name in matching_files:
39
+ entries_to_remove.append(entry)
40
+
41
+ for entry in entries_to_remove:
42
+ docket.remove(entry)
43
+ removed_count += 1
44
+ logger.debug(f"Removed from docket: {entry.name}")
45
+
46
+ # Save updated docket back to file
47
+ if removed_count > 0:
48
+ json_data = json.dumps(docket.get_entries(), indent=2)
49
+ with open(app_dirs.file_docket_path, "w") as f:
50
+ f.write(json_data)
51
+ logger.info(f"Updated file_docket.json, removed {removed_count} entries")
52
+
53
+ return removed_count
54
+
55
+ except Exception as e:
56
+ logger.error(f"Failed to remove files from file_docket: {e}")
57
+ logger.exception(e)
58
+ return 0
59
+
60
+
61
+ def remove_files_from_kb(project_name: str, filename_pattern: str, confirm: bool = True) -> int:
62
+ """
63
+ Remove files from the ChromaDB knowledge base that match the given filename pattern.
64
+
65
+ Args:
66
+ project_name: Name of the project
67
+ filename_pattern: Full or partial filename to match (case-insensitive)
68
+ confirm: If True, ask for confirmation before deleting
69
+
70
+ Returns:
71
+ Number of chunks deleted
72
+ """
73
+ try:
74
+ with Session() as db:
75
+ chroma_client = get_chromadb_client_for_project_name(db, project_name)
76
+
77
+ # Get all documents from the collection
78
+ collection = chroma_client._collection
79
+ results = collection.get()
80
+
81
+ if not results or not results.get('metadatas'):
82
+ logger.info("No files found in the knowledge base")
83
+ print("No files found in the knowledge base.")
84
+ return 0
85
+
86
+ # Find matching file chunks
87
+ matching_ids: List[str] = []
88
+ matching_files: set = set()
89
+
90
+ for idx, metadata in enumerate(results['metadatas']):
91
+ if metadata:
92
+ file_name = metadata.get('file_name', '')
93
+ # Case-insensitive partial match
94
+ if filename_pattern.lower() in file_name.lower():
95
+ matching_ids.append(results['ids'][idx])
96
+ matching_files.add(file_name)
97
+
98
+ if not matching_ids:
99
+ print(f"\nNo files matching '{filename_pattern}' found in the knowledge base.")
100
+ return 0
101
+
102
+ # Display matching files
103
+ print(f"\nFound {len(matching_files)} file(s) matching '{filename_pattern}':")
104
+ for file_name in sorted(matching_files):
105
+ print(f" • {file_name}")
106
+ print(f"\nTotal chunks to delete: {len(matching_ids)}")
107
+
108
+ # Confirm deletion
109
+ if confirm:
110
+ response = input("\nAre you sure you want to delete these files? (yes/no): ")
111
+ if response.lower() not in ['yes', 'y']:
112
+ print("Deletion cancelled.")
113
+ return 0
114
+
115
+ # Delete the chunks from ChromaDB
116
+ collection.delete(ids=matching_ids)
117
+
118
+ # Remove files from file_docket
119
+ removed_from_docket = _remove_from_file_docket(matching_files)
120
+
121
+ print(f"\nSuccessfully deleted {len(matching_ids)} chunks from {len(matching_files)} file(s).")
122
+ if removed_from_docket > 0:
123
+ print(f"Removed {removed_from_docket} file(s) from file_docket.json")
124
+ logger.info(f"Deleted {len(matching_ids)} chunks from {len(matching_files)} files matching '{filename_pattern}'")
125
+ logger.info(f"Removed {removed_from_docket} entries from file_docket")
126
+
127
+ return len(matching_ids)
128
+
129
+ except Exception as e:
130
+ logger.error(f"Failed to remove files from knowledge base for project: {project_name}")
131
+ logger.exception(e)
132
+ print(f"\nError: Failed to remove files. Check logs for details.")
133
+ raise
134
+
135
+
136
+ def list_matching_files(project_name: str, filename_pattern: str) -> List[str]:
137
+ """
138
+ List files that match the given filename pattern without deleting them.
139
+
140
+ Args:
141
+ project_name: Name of the project
142
+ filename_pattern: Full or partial filename to match (case-insensitive)
143
+
144
+ Returns:
145
+ List of matching filenames
146
+ """
147
+ try:
148
+ with Session() as db:
149
+ chroma_client = get_chromadb_client_for_project_name(db, project_name)
150
+
151
+ # Get all documents from the collection
152
+ collection = chroma_client._collection
153
+ results = collection.get()
154
+
155
+ if not results or not results.get('metadatas'):
156
+ return []
157
+
158
+ # Find matching files
159
+ matching_files: set = set()
160
+
161
+ for metadata in results['metadatas']:
162
+ if metadata:
163
+ file_name = metadata.get('file_name', '')
164
+ # Case-insensitive partial match
165
+ if filename_pattern.lower() in file_name.lower():
166
+ matching_files.add(file_name)
167
+
168
+ return sorted(list(matching_files))
169
+
170
+ except Exception as e:
171
+ logger.error(f"Failed to list matching files for project: {project_name}")
172
+ logger.exception(e)
173
+ return []
mfcli/utils/logger.py ADDED
@@ -0,0 +1,28 @@
1
+ import logging
2
+ import sys
3
+
4
+ from mfcli.utils.config import get_config
5
+
6
+
7
+ def setup_logging():
8
+ config = get_config()
9
+ formatter = logging.Formatter(
10
+ fmt="%(asctime)s [%(levelname)s] %(name)s:%(funcName)s:%(lineno)d: %(message)s",
11
+ datefmt="%Y-%m-%d %H:%M:%S"
12
+ )
13
+
14
+ handler = logging.StreamHandler(sys.stdout)
15
+ handler.setFormatter(formatter)
16
+
17
+ logger = logging.getLogger()
18
+ logger.setLevel(config.log_level)
19
+ logger.addHandler(handler)
20
+ logger.propagate = False
21
+
22
+ logging.getLogger("google_genai").setLevel(logging.ERROR)
23
+ logging.getLogger("httpx").setLevel(logging.ERROR)
24
+ logging.getLogger("docling").setLevel(logging.ERROR)
25
+
26
+
27
+ def get_logger(name=None):
28
+ return logging.getLogger(name or __name__)
@@ -0,0 +1,311 @@
1
+ """MCP server auto-configuration for Cline and Claude Code."""
2
+ import json
3
+ import os
4
+ import platform
5
+ import shutil
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import List, Tuple, Optional
9
+
10
+
11
+ def get_mcp_config_paths() -> List[Tuple[str, Path]]:
12
+ """Get potential MCP configuration file paths for different editors."""
13
+ paths = []
14
+ system = platform.system()
15
+
16
+ if system == "Windows":
17
+ appdata = Path(os.environ.get("APPDATA", ""))
18
+ localappdata = Path(os.environ.get("LOCALAPPDATA", ""))
19
+
20
+ # Cline (VS Code extension)
21
+ cline_vscode = appdata / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json"
22
+ if cline_vscode.exists():
23
+ paths.append(("Cline (VS Code)", cline_vscode))
24
+
25
+ # Windsurf/Cline standalone
26
+ home = Path.home()
27
+ cline_standalone = home / ".cline" / "mcp_settings.json"
28
+ if cline_standalone.exists():
29
+ paths.append(("Cline (Standalone)", cline_standalone))
30
+
31
+ # Claude Code (if it exists on Windows - need to verify path)
32
+ # This is a placeholder - actual path may differ
33
+ claude_code = localappdata / "Claude" / "mcp_settings.json"
34
+ if claude_code.exists():
35
+ paths.append(("Claude Code", claude_code))
36
+
37
+ elif system in ["Darwin", "Linux"]: # macOS or Linux
38
+ home = Path.home()
39
+
40
+ # Cline (VS Code extension) - macOS
41
+ if system == "Darwin":
42
+ cline_vscode = home / "Library" / "Application Support" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json"
43
+ else: # Linux
44
+ cline_vscode = home / ".config" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json"
45
+
46
+ if cline_vscode.exists():
47
+ paths.append(("Cline (VS Code)", cline_vscode))
48
+
49
+ # Windsurf/Cline standalone
50
+ cline_standalone = home / ".cline" / "mcp_settings.json"
51
+ if cline_standalone.exists():
52
+ paths.append(("Cline (Standalone)", cline_standalone))
53
+
54
+ # Claude Code
55
+ claude_code = home / ".claude" / "mcp_settings.json"
56
+ if claude_code.exists():
57
+ paths.append(("Claude Code", claude_code))
58
+
59
+ return paths
60
+
61
+
62
+ def backup_config(config_path: Path) -> Path:
63
+ """Create a backup of the configuration file."""
64
+ backup_path = config_path.with_suffix(config_path.suffix + ".backup")
65
+ shutil.copy2(config_path, backup_path)
66
+ return backup_path
67
+
68
+
69
+ def get_mfcli_mcp_config() -> dict:
70
+ """Get the mfcli-mcp server configuration."""
71
+ return {
72
+ "mfcli-mcp": {
73
+ "disabled": False,
74
+ "timeout": 60,
75
+ "type": "stdio",
76
+ "command": "mfcli-mcp"
77
+ }
78
+ }
79
+
80
+
81
+ def update_mcp_config(config_path: Path) -> bool:
82
+ """Update MCP configuration file with mfcli-mcp server."""
83
+ try:
84
+ # Read existing config
85
+ with open(config_path, 'r') as f:
86
+ config = json.load(f)
87
+
88
+ # Ensure mcpServers key exists
89
+ if "mcpServers" not in config:
90
+ config["mcpServers"] = {}
91
+
92
+ # Check if mfcli-mcp already exists
93
+ if "mfcli-mcp" in config["mcpServers"]:
94
+ print(f" ℹ️ mfcli-mcp already configured in this file")
95
+ return True
96
+
97
+ # Add mfcli-mcp configuration
98
+ mfcli_config = get_mfcli_mcp_config()
99
+ config["mcpServers"].update(mfcli_config)
100
+
101
+ # Create backup
102
+ backup_path = backup_config(config_path)
103
+ print(f" 📋 Backup created: {backup_path}")
104
+
105
+ # Write updated config
106
+ with open(config_path, 'w') as f:
107
+ json.dump(config, f, indent=2)
108
+
109
+ return True
110
+
111
+ except Exception as e:
112
+ print(f" ❌ Error updating config: {e}")
113
+ return False
114
+
115
+
116
+ def create_mcp_config(config_path: Path) -> bool:
117
+ """Create a new MCP configuration file with mfcli-mcp server."""
118
+ try:
119
+ # Ensure parent directory exists
120
+ config_path.parent.mkdir(parents=True, exist_ok=True)
121
+
122
+ # Create new config
123
+ config = {
124
+ "mcpServers": get_mfcli_mcp_config()
125
+ }
126
+
127
+ # Write config
128
+ with open(config_path, 'w') as f:
129
+ json.dump(config, f, indent=2)
130
+
131
+ return True
132
+
133
+ except Exception as e:
134
+ print(f" ❌ Error creating config: {e}")
135
+ return False
136
+
137
+
138
+ def verify_mfcli_installation() -> bool:
139
+ """Verify that mfcli-mcp is installed and accessible."""
140
+ try:
141
+ # Check if mfcli-mcp is in PATH
142
+ result = shutil.which("mfcli-mcp")
143
+ if result:
144
+ return True
145
+
146
+ # On Windows, also check Scripts directory
147
+ if platform.system() == "Windows":
148
+ scripts_dir = Path(sys.executable).parent / "Scripts"
149
+ mfcli_mcp = scripts_dir / "mfcli-mcp.exe"
150
+ if mfcli_mcp.exists():
151
+ return True
152
+
153
+ return False
154
+
155
+ except Exception:
156
+ return False
157
+
158
+
159
+ def test_mcp_server() -> bool:
160
+ """Test if MCP server can be started."""
161
+ print(" Testing MCP server...", end=' ')
162
+ sys.stdout.flush()
163
+
164
+ try:
165
+ import subprocess
166
+ # Try to run mfcli-mcp with a timeout
167
+ result = subprocess.run(
168
+ ["mfcli-mcp"],
169
+ capture_output=True,
170
+ timeout=5,
171
+ text=True
172
+ )
173
+ # If it starts without error, that's good enough
174
+ print("✅")
175
+ return True
176
+ except subprocess.TimeoutExpired:
177
+ # Timeout is actually OK - it means the server started
178
+ print("✅")
179
+ return True
180
+ except FileNotFoundError:
181
+ print("❌ mfcli-mcp command not found")
182
+ return False
183
+ except Exception as e:
184
+ print(f"❌ {str(e)[:50]}")
185
+ return False
186
+
187
+
188
+ def setup_mcp_servers() -> None:
189
+ """Auto-configure MCP servers for detected editors."""
190
+ print("\n" + "="*70)
191
+ print(" MCP SERVER AUTO-CONFIGURATION")
192
+ print("="*70)
193
+ print("\n Detecting installed AI coding assistants...")
194
+
195
+ # Verify mfcli installation
196
+ if not verify_mfcli_installation():
197
+ print("\n ❌ mfcli-mcp command not found!")
198
+ print("\n Please ensure mfcli is installed with:")
199
+ print(" pipx install mfcli")
200
+ print("\n Or if installing from source:")
201
+ print(" pip install .")
202
+ print("="*70 + "\n")
203
+ return
204
+
205
+ # Get configuration paths
206
+ config_paths = get_mcp_config_paths()
207
+
208
+ if not config_paths:
209
+ print("\n ℹ️ No AI coding assistants detected.")
210
+ print("\n Supported editors:")
211
+ print(" - Cline (VS Code extension)")
212
+ print(" - Cline (Standalone)")
213
+ print(" - Claude Code")
214
+ print("\n If you have one of these installed, the configuration file may")
215
+ print(" not exist yet. You can create it manually at:")
216
+
217
+ system = platform.system()
218
+ if system == "Windows":
219
+ print("\n Cline (VS Code):")
220
+ print(" %APPDATA%\\Code\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json")
221
+ print("\n Cline (Standalone):")
222
+ print(" %USERPROFILE%\\.cline\\mcp_settings.json")
223
+ else:
224
+ print("\n Cline (VS Code):")
225
+ if system == "Darwin":
226
+ print(" ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json")
227
+ else:
228
+ print(" ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json")
229
+ print("\n Cline (Standalone):")
230
+ print(" ~/.cline/mcp_settings.json")
231
+ print("\n Claude Code:")
232
+ print(" ~/.claude/mcp_settings.json")
233
+
234
+ print("\n Then run this command again.")
235
+ print("="*70 + "\n")
236
+ return
237
+
238
+ print(f"\n Found {len(config_paths)} configuration file(s):\n")
239
+
240
+ success_count = 0
241
+ for name, path in config_paths:
242
+ print(f" 📝 {name}")
243
+ print(f" {path}")
244
+
245
+ if update_mcp_config(path):
246
+ print(f" ✅ Successfully configured!\n")
247
+ success_count += 1
248
+ else:
249
+ print(f" ❌ Configuration failed\n")
250
+
251
+ if success_count > 0:
252
+ print("="*70)
253
+ print(f" ✅ Successfully configured {success_count} editor(s)!")
254
+ print("="*70)
255
+ print("\n Next steps:")
256
+ print(" 1. Restart your editor (VS Code, Cline, etc.)")
257
+ print(" 2. The mfcli-mcp server should now be available")
258
+ print(" 3. Try using the 'query_local_rag' tool in your AI assistant")
259
+ print("\n To test the MCP server:")
260
+ print(" mfcli doctor")
261
+ print("\n")
262
+ else:
263
+ print("="*70)
264
+ print(" ⚠️ No configurations were updated.")
265
+ print("="*70 + "\n")
266
+
267
+
268
+ def get_manual_setup_instructions() -> str:
269
+ """Get manual MCP setup instructions."""
270
+ instructions = """
271
+ Manual MCP Setup Instructions
272
+ ==============================
273
+
274
+ If auto-configuration didn't work, you can manually add mfcli-mcp to your
275
+ editor's MCP configuration file.
276
+
277
+ 1. Locate your MCP configuration file:
278
+
279
+ Windows (Cline in VS Code):
280
+ %APPDATA%\\Code\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json
281
+
282
+ macOS (Cline in VS Code):
283
+ ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
284
+
285
+ Linux (Cline in VS Code):
286
+ ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
287
+
288
+ Cline Standalone:
289
+ ~/.cline/mcp_settings.json
290
+
291
+ Claude Code:
292
+ ~/.claude/mcp_settings.json
293
+
294
+ 2. Add the following configuration to the "mcpServers" section:
295
+
296
+ {
297
+ "mcpServers": {
298
+ "mfcli-mcp": {
299
+ "disabled": false,
300
+ "timeout": 60,
301
+ "type": "stdio",
302
+ "command": "mfcli-mcp"
303
+ }
304
+ }
305
+ }
306
+
307
+ 3. Save the file and restart your editor.
308
+
309
+ 4. The mfcli-mcp server should now be available in your AI assistant.
310
+ """
311
+ return instructions
@@ -0,0 +1,18 @@
1
+ from pathlib import Path
2
+
3
+ from mfcli.utils.files import file_access_check
4
+ from mfcli.utils.logger import get_logger
5
+
6
+ logger = get_logger(__name__)
7
+
8
+
9
+ def run_migrations():
10
+ from alembic.config import Config
11
+ from alembic import command
12
+
13
+ config_file_path = Path(__file__).parent.parent / "alembic.ini"
14
+ if not file_access_check(config_file_path):
15
+ raise RuntimeError(f"Could not find Alembic config file path: {config_file_path}")
16
+
17
+ alembic_cfg = Config(config_file_path)
18
+ command.upgrade(alembic_cfg, "head")
mfcli/utils/orm.py ADDED
@@ -0,0 +1,43 @@
1
+ from functools import lru_cache
2
+
3
+ from sqlalchemy import create_engine, event
4
+ from sqlalchemy.orm import sessionmaker, Session as dbSession
5
+
6
+ from mfcli.models.base import Base
7
+ from mfcli.models.bom import BOM
8
+
9
+ from mfcli.utils.config import get_config
10
+ from mfcli.utils.directory_manager import app_dirs
11
+
12
+ config = get_config()
13
+
14
+
15
+ @lru_cache
16
+ def get_db_url() -> str:
17
+ return f"sqlite:///{app_dirs.app_data_dir / "multifactor.db"}"
18
+
19
+
20
+ engine = create_engine(
21
+ get_db_url(),
22
+ pool_pre_ping=True,
23
+ pool_recycle=1800,
24
+ pool_size=10,
25
+ max_overflow=20,
26
+ pool_timeout=30
27
+ )
28
+
29
+
30
+ @event.listens_for(engine, "connect")
31
+ def enable_sqlite_fk(dbapi_conn, conn_record):
32
+ cursor = dbapi_conn.cursor()
33
+ cursor.execute("PRAGMA foreign_keys=ON;")
34
+ cursor.close()
35
+
36
+
37
+ Session = sessionmaker(bind=engine)
38
+ session = dbSession(engine)
39
+
40
+
41
+ def create_orm():
42
+ engine.connect()
43
+ Base.metadata.create_all(engine)
@@ -0,0 +1,63 @@
1
+ import tempfile
2
+ from pathlib import Path
3
+ from uuid import uuid4
4
+
5
+ import pikepdf
6
+ from io import BytesIO
7
+
8
+ from mfcli.utils.logger import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ class PDFSplitter:
14
+ def __init__(self, file_name: str, content: bytes):
15
+ self._name = file_name
16
+ self._content = content
17
+
18
+ @staticmethod
19
+ def _head_page_limit(total_pages: int) -> int:
20
+ if total_pages <= 30:
21
+ return 10
22
+ elif total_pages <= 100:
23
+ return 20
24
+ else:
25
+ return 30
26
+
27
+ def _open_pdf(self) -> pikepdf.Pdf:
28
+ return pikepdf.open(BytesIO(self._content))
29
+
30
+ def split_pdf_head(self) -> Path:
31
+ with self._open_pdf() as src:
32
+ total_pages = len(src.pages)
33
+ page_limit = self._head_page_limit(total_pages)
34
+
35
+ dst = pikepdf.Pdf.new()
36
+ dst.pages.extend(src.pages[:page_limit])
37
+
38
+ output_path = Path(tempfile.mktemp(suffix=".pdf"))
39
+ dst.save(output_path)
40
+
41
+ return output_path
42
+
43
+ def extract_range(
44
+ self,
45
+ start_page: int,
46
+ end_page: int,
47
+ output_folder: Path,
48
+ ) -> Path:
49
+ logger.debug(f"Splitting PDF: {self._name}")
50
+
51
+ output_folder.mkdir(parents=True, exist_ok=True)
52
+
53
+ with self._open_pdf() as src:
54
+ dst = pikepdf.Pdf.new()
55
+ dst.pages.extend(src.pages[start_page:end_page + 1])
56
+
57
+ output_path = output_folder / f"{uuid4().hex}.pdf"
58
+ dst.save(output_path)
59
+
60
+ logger.debug(f"Output PDF part to: {output_path}")
61
+ logger.debug(f"PDF splitter finished: {self._name}")
62
+
63
+ return output_path
@@ -0,0 +1,22 @@
1
+ from typing import Type, List, Any
2
+
3
+ from mfcli.models.base import Base
4
+ from mfcli.utils.orm import Session
5
+
6
+
7
+ class QueryService:
8
+ def __init__(self, db: Session):
9
+ self._db = db
10
+
11
+ def query_all(
12
+ self,
13
+ entity_type: Type[Base],
14
+ filters: List[Any] | None = None,
15
+ order_by: Any | None = None
16
+ ) -> List[Base]:
17
+ query = self._db.query(entity_type)
18
+ if filters:
19
+ query = query.filter(filters)
20
+ if order_by:
21
+ query = query.order_by(order_by)
22
+ return query.all()