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.
- mfcli/.env.example +72 -0
- mfcli/__init__.py +0 -0
- mfcli/agents/__init__.py +0 -0
- mfcli/agents/controller/__init__.py +0 -0
- mfcli/agents/controller/agent.py +19 -0
- mfcli/agents/controller/config.yaml +27 -0
- mfcli/agents/controller/tools.py +42 -0
- mfcli/agents/tools/general.py +118 -0
- mfcli/alembic/env.py +61 -0
- mfcli/alembic/script.py.mako +28 -0
- mfcli/alembic/versions/6ccc0c7c397c_added_fields_to_pdf_parts_model.py +39 -0
- mfcli/alembic/versions/769019ef4870_added_gemini_file_path_to_pdf_part_model.py +33 -0
- mfcli/alembic/versions/7a2e3a779fdc_added_functional_block_and_component_.py +54 -0
- mfcli/alembic/versions/7d5adb2a47a7_added_pdf_parts_model.py +41 -0
- mfcli/alembic/versions/7fcb7d6a5836_init.py +167 -0
- mfcli/alembic/versions/e0f2b5765c72_added_cascade_delete_for_models_that_.py +32 -0
- mfcli/alembic.ini +147 -0
- mfcli/cli/__init__.py +0 -0
- mfcli/cli/dependencies.py +59 -0
- mfcli/cli/main.py +192 -0
- mfcli/client/__init__.py +0 -0
- mfcli/client/chroma_db.py +184 -0
- mfcli/client/docling.py +44 -0
- mfcli/client/gemini.py +252 -0
- mfcli/client/llama_parse.py +38 -0
- mfcli/client/vector_db.py +93 -0
- mfcli/constants/__init__.py +0 -0
- mfcli/constants/base_enum.py +18 -0
- mfcli/constants/directory_names.py +1 -0
- mfcli/constants/file_types.py +189 -0
- mfcli/constants/gemini.py +1 -0
- mfcli/constants/openai.py +6 -0
- mfcli/constants/pipeline_run_status.py +3 -0
- mfcli/crud/__init__.py +0 -0
- mfcli/crud/file.py +42 -0
- mfcli/crud/functional_blocks.py +26 -0
- mfcli/crud/netlist.py +18 -0
- mfcli/crud/pipeline_run.py +17 -0
- mfcli/crud/project.py +99 -0
- mfcli/digikey/__init__.py +0 -0
- mfcli/digikey/digikey.py +105 -0
- mfcli/main.py +5 -0
- mfcli/mcp/__init__.py +0 -0
- mfcli/mcp/configs/cline_mcp_settings.json +11 -0
- mfcli/mcp/configs/mfcli.mcp.json +7 -0
- mfcli/mcp/mcp_instance.py +6 -0
- mfcli/mcp/server.py +37 -0
- mfcli/mcp/state_manager.py +51 -0
- mfcli/mcp/tools/__init__.py +0 -0
- mfcli/mcp/tools/query_knowledgebase.py +108 -0
- mfcli/models/__init__.py +10 -0
- mfcli/models/base.py +10 -0
- mfcli/models/bom.py +71 -0
- mfcli/models/datasheet.py +10 -0
- mfcli/models/debug_setup.py +64 -0
- mfcli/models/file.py +43 -0
- mfcli/models/file_docket.py +94 -0
- mfcli/models/file_metadata.py +19 -0
- mfcli/models/functional_blocks.py +94 -0
- mfcli/models/llm_response.py +5 -0
- mfcli/models/mcu.py +97 -0
- mfcli/models/mcu_errata.py +26 -0
- mfcli/models/netlist.py +59 -0
- mfcli/models/pdf_parts.py +25 -0
- mfcli/models/pipeline_run.py +34 -0
- mfcli/models/project.py +27 -0
- mfcli/models/project_metadata.py +15 -0
- mfcli/pipeline/__init__.py +0 -0
- mfcli/pipeline/analysis/__init__.py +0 -0
- mfcli/pipeline/analysis/bom_netlist_mapper.py +28 -0
- mfcli/pipeline/analysis/generators/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/bom/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/bom/bom.py +74 -0
- mfcli/pipeline/analysis/generators/debug_setup/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/debug_setup/debug_setup.py +71 -0
- mfcli/pipeline/analysis/generators/debug_setup/instructions.py +150 -0
- mfcli/pipeline/analysis/generators/functional_blocks/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/functional_blocks/functional_blocks.py +93 -0
- mfcli/pipeline/analysis/generators/functional_blocks/instructions.py +34 -0
- mfcli/pipeline/analysis/generators/functional_blocks/validator.py +94 -0
- mfcli/pipeline/analysis/generators/generator.py +258 -0
- mfcli/pipeline/analysis/generators/generator_base.py +18 -0
- mfcli/pipeline/analysis/generators/mcu/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/mcu/instructions.py +156 -0
- mfcli/pipeline/analysis/generators/mcu/mcu.py +84 -0
- mfcli/pipeline/analysis/generators/mcu_errata/__init__.py +1 -0
- mfcli/pipeline/analysis/generators/mcu_errata/instructions.py +77 -0
- mfcli/pipeline/analysis/generators/mcu_errata/mcu_errata.py +95 -0
- mfcli/pipeline/analysis/generators/summary/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/summary/summary.py +47 -0
- mfcli/pipeline/classifier.py +93 -0
- mfcli/pipeline/data_enricher.py +15 -0
- mfcli/pipeline/extractor.py +34 -0
- mfcli/pipeline/extractors/__init__.py +0 -0
- mfcli/pipeline/extractors/pdf.py +12 -0
- mfcli/pipeline/parser.py +120 -0
- mfcli/pipeline/parsers/__init__.py +0 -0
- mfcli/pipeline/parsers/netlist/__init__.py +0 -0
- mfcli/pipeline/parsers/netlist/edif.py +93 -0
- mfcli/pipeline/parsers/netlist/kicad_legacy_net.py +326 -0
- mfcli/pipeline/parsers/netlist/kicad_spice.py +135 -0
- mfcli/pipeline/parsers/netlist/pads.py +185 -0
- mfcli/pipeline/parsers/netlist/protel.py +166 -0
- mfcli/pipeline/parsers/netlist/protel_detector.py +29 -0
- mfcli/pipeline/pipeline.py +419 -0
- mfcli/pipeline/preprocessors/__init__.py +0 -0
- mfcli/pipeline/preprocessors/user_guide.py +127 -0
- mfcli/pipeline/run_context.py +32 -0
- mfcli/pipeline/schema_mapper.py +89 -0
- mfcli/pipeline/sub_classifier.py +115 -0
- mfcli/utils/__init__.py +0 -0
- mfcli/utils/config.py +33 -0
- mfcli/utils/configurator.py +324 -0
- mfcli/utils/data_cleaner.py +82 -0
- mfcli/utils/datasheet_vectorizer.py +281 -0
- mfcli/utils/directory_manager.py +96 -0
- mfcli/utils/file_upload.py +298 -0
- mfcli/utils/files.py +16 -0
- mfcli/utils/http_requests.py +54 -0
- mfcli/utils/kb_lister.py +89 -0
- mfcli/utils/kb_remover.py +173 -0
- mfcli/utils/logger.py +28 -0
- mfcli/utils/mcp_configurator.py +311 -0
- mfcli/utils/migrations.py +18 -0
- mfcli/utils/orm.py +43 -0
- mfcli/utils/pdf_splitter.py +63 -0
- mfcli/utils/query_service.py +22 -0
- mfcli/utils/system_check.py +306 -0
- mfcli/utils/tools.py +31 -0
- mfcli/utils/vectorizer.py +28 -0
- mfcli-0.2.0.dist-info/METADATA +841 -0
- mfcli-0.2.0.dist-info/RECORD +136 -0
- mfcli-0.2.0.dist-info/WHEEL +5 -0
- mfcli-0.2.0.dist-info/entry_points.txt +3 -0
- mfcli-0.2.0.dist-info/licenses/LICENSE +21 -0
- 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()
|