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,189 @@
1
+ from typing import Literal, Dict
2
+
3
+ from mfcli.constants.base_enum import BaseEnum
4
+
5
+ FILE_SUBTYPE_UNKNOWN = "UNKNOWN"
6
+
7
+
8
+ class FileTypes(BaseEnum):
9
+ CSV = 1
10
+ ASC = 2
11
+ NET = 3
12
+ CIR = 4
13
+ PDF = 5
14
+
15
+
16
+ class FileSubtypes(BaseEnum):
17
+ BOM = 1
18
+ PADS_PCB_ASCII = 2
19
+ KICAD_LEGACY_NET = 3
20
+ KICAD_SPICE = 4
21
+ SCHEMATIC = 5
22
+ ERRATA = 6
23
+ MCU_DATASHEET = 7
24
+ PROTEL_ALTIUM = 8
25
+ GENERAL_DATASHEET = 9
26
+ UNKNOWN = 10
27
+ USER_GUIDE = 11
28
+ REFERENCE_MANUAL = 12
29
+
30
+
31
+ # PDF subtypes which require summary cheat sheets
32
+ SummaryCheatSheetSubtypes: set[FileSubtypes] = {
33
+ FileSubtypes.USER_GUIDE,
34
+ FileSubtypes.REFERENCE_MANUAL
35
+ }
36
+
37
+ # PDF file subtypes that do not require docling text extraction or vectorization (example, schematic files)
38
+ PDFNoVectorizeFileSubtypes: set[FileSubtypes] = {
39
+ FileSubtypes.SCHEMATIC
40
+ }
41
+
42
+ # File subtypes that have no schema to parse
43
+ SchemalessFileSubtypes: set[FileSubtypes] = {
44
+ FileSubtypes.SCHEMATIC,
45
+ FileSubtypes.ERRATA,
46
+ FileSubtypes.MCU_DATASHEET,
47
+ FileSubtypes.USER_GUIDE,
48
+ FileSubtypes.REFERENCE_MANUAL
49
+ }
50
+
51
+ # File subtype names are for use with LLM, validating subtype response
52
+
53
+ PDFFileSubtypeNames = Literal[
54
+ 'SCHEMATIC',
55
+ 'ERRATA',
56
+ 'MCU_DATASHEET',
57
+ 'GENERAL_DATASHEET',
58
+ 'USER_GUIDE',
59
+ 'REFERENCE_MANUAL',
60
+ 'UNKNOWN'
61
+ ]
62
+
63
+ OtherFileSubtypeNames = Literal[
64
+ 'BOM',
65
+ 'PADS_PCB_ASCII',
66
+ 'KICAD_LEGACY_NET',
67
+ 'KICAD_SPICE',
68
+ 'PROTEL_ALTIUM'
69
+ ]
70
+
71
+ PDFMimeTypes = {
72
+ "application/pdf",
73
+ "application/x-pdf",
74
+ "application/acrobat",
75
+ "applications/vnd.pdf",
76
+ "application/nappdf",
77
+ }
78
+
79
+ SupportedFileTypes = {
80
+ "CSV": {
81
+ "mime_types": {
82
+ "text/csv",
83
+ "text/plain"
84
+ },
85
+ "subtypes": {
86
+ "BOM"
87
+ }
88
+ },
89
+ "ASC": {
90
+ "mime_types": {
91
+ "text/plain"
92
+ },
93
+ "subtypes": {
94
+ "PADS_PCB_ASCII"
95
+ }
96
+ },
97
+ "NET": {
98
+ "mime_types": {
99
+ "text/plain"
100
+ },
101
+ "subtypes": {
102
+ "KICAD_LEGACY_NET",
103
+ "PROTEL_ALTIUM"
104
+ }
105
+ },
106
+ "CIR": {
107
+ "mime_types": {
108
+ "text/plain"
109
+ },
110
+ "subtypes": {
111
+ "KICAD_SPICE"
112
+ }
113
+ },
114
+ "PDF": {
115
+ "mime_types": PDFMimeTypes,
116
+ "subtypes": {
117
+ "SCHEMATIC",
118
+ "ERRATA",
119
+ "MCU_DATASHEET",
120
+ "REFERENCE_MANUAL"
121
+ }
122
+ }
123
+ }
124
+
125
+ # File subtype descriptions (for use in file subtype auto-discovery)
126
+
127
+ # File subtypes for which we have parsers and do not the LLM to discover subtype
128
+ KnownFileSubtypeDescriptions: Dict[SupportedFileTypes, Dict[str, str]] = {
129
+ "KICAD_LEGACY_NET": {
130
+ "name": 'KiCad Legacy Netlist',
131
+ "description": "Older KiCad netlist format containing components, nets, pins, and footprint references using plain text blocks like (comp ...), (net ...), and (pin ...); does not contain SPICE commands or PCB geometry."
132
+ },
133
+ "PROTEL_ALTIUM": {
134
+ "name": "Protel/Altium Designer Netlist",
135
+ "description": "Hierarchical bracketed netlist with components, footprints, and pin-to-net connections using sections like [Component] and [Net]; verbose structure unique to Altium/Protel; not a PCB layout or SPICE file."
136
+ }
137
+ }
138
+
139
+ # Only PDF files
140
+ PDFSubtypeDescriptions: Dict[SupportedFileTypes, Dict[str, str]] = {
141
+ "SCHEMATIC": {
142
+ "name": "Schematic",
143
+ "description": "Diagrammatic circuit representation containing symbols, wires, net labels, sheet numbers, and component reference designators; typically PDF or image-based; not a table, netlist, PCB file, or simulation file."
144
+ },
145
+ "ERRATA": {
146
+ "name": "Engineering Errata File",
147
+ "description": "An ERRATA document lists known defects, mistakes, missing features, and silicon bugs in a released product. It is NOT a datasheet, does NOT describe product specifications, and consists of issue-by-issue bullet points with IDs, status, and workarounds. It never contains full electrical characteristics tables, application diagrams, or packaging information."
148
+ },
149
+ "MCU_DATASHEET": {
150
+ "name": "MCU Datasheet",
151
+ "description": "Microcontroller technical reference containing CPU architecture, memory maps, peripheral descriptions, electrical characteristics, pinouts, timing diagrams, and register information; exclusive to microcontrollers."
152
+ },
153
+ "GENERAL_DATASHEET": {
154
+ "name": "General Component Datasheet",
155
+ "description": "Datasheet for non-MCU components such as ICs, sensors, regulators, passives, and connectors, containing electrical specs, pin descriptions, operating conditions, and application circuits; no CPU or register-map content. A general component datasheet is a large, structured document containing electrical specifications, tables, operating ranges, application circuits, diagrams, pinouts, mechanical drawings, and packaging information. It is not a list of defects and does not describe known problems."
156
+ },
157
+ "USER_GUIDE": {
158
+ "name": "User Guide",
159
+ "description": "A practical, instructional document that explains how to use a hardware device, development board, evaluation kit, or module. A user guide focuses on setup, configuration, jumper settings, power requirements, interfaces, connectors, example usage, and safety notes. Unlike a datasheet, it avoids deep electrical specifications, register maps, or silicon details, and instead provides step-by-step procedures, diagrams, block overviews, and usage instructions intended for end users or developers."
160
+ },
161
+ "REFERENCE_MANUAL": {
162
+ "name": "Hardware Reference Manual",
163
+ "description": "A detailed, authoritative technical manual that defines the internal hardware architecture and low-level behavior of a device or system. It provides exhaustive functional descriptions of hardware blocks, registers, memory maps, address spaces, bit fields, timing relationships, and control logic. A hardware reference manual is used by firmware and driver developers for direct hardware interaction. It is not a datasheet, does not focus on electrical specifications or marketing summaries, and is not a step-by-step user guide."
164
+ },
165
+ "UNKNOWN": {
166
+ "name": "Unknown File",
167
+ "description": "File content does not match any known category such as BOM, PCB, netlist, schematic, datasheet, SPICE, or errata; used when insufficient or ambiguous information is present."
168
+ }
169
+ }
170
+
171
+ # Any other files subtypes
172
+ OtherFileTypeDescriptions: Dict[SupportedFileTypes, Dict[str, str]] = {
173
+ "BOM": {
174
+ "name": "Bill of Materials (BOM)",
175
+ "description": "Tabular component list containing part numbers, quantities, manufacturers, and descriptions; typically CSV/XLSX rows and columns; does not contain nets, schematic symbols, PCB layout, or SPICE commands."
176
+ },
177
+ "PADS_PCB_ASCII": {
178
+ "name": "PADS ASCII PCB",
179
+ "description": "ASCII export of a PADS PCB layout containing footprints, padstacks, copper geometry, vias, XY coordinates, and board definitions; does not contain schematic symbols, part tables, or SPICE simulation directives."
180
+ },
181
+ "KICAD_SPICE": {
182
+ "name": "KiCad SPICE Netlist",
183
+ "description": "SPICE circuit simulation file containing directives (.tran, .ac, .dc, .include), models, and device lines (R, C, L, V, I elements); does not contain PCB footprints, tables, or datasheet content."
184
+ },
185
+ "UNKNOWN": {
186
+ "name": "Unknown File",
187
+ "description": "File content does not match any known category such as BOM, PCB, netlist, schematic, datasheet, SPICE, or errata; used when insufficient or ambiguous information is present."
188
+ }
189
+ }
@@ -0,0 +1 @@
1
+ MAX_GEMINI_PAGES = 1000
@@ -0,0 +1,6 @@
1
+ OPENAI_MAX_ENCODING_REQUEST_TOKENS = 250000
2
+ OPENAI_ENCODING_MODEL = "cl100k_base"
3
+ OPENAI_MAX_TOKENS_PER_CHUNK = 8192
4
+ OPENAI_MAX_WINDOW_SIZE = 128 * 1024
5
+ OPENAI_DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large"
6
+ OPENAI_DEFAULT_EMBEDDING_DIMENSIONS = 3072
@@ -0,0 +1,3 @@
1
+ PIPELINE_STATUS_IN_PROGRESS = 0
2
+ PIPELINE_STATUS_COMPLETE = 1
3
+ PIPELINE_STATUS_FAILURE = 2
mfcli/crud/__init__.py ADDED
File without changes
mfcli/crud/file.py ADDED
@@ -0,0 +1,42 @@
1
+ from mfcli.models.file import File
2
+ from mfcli.models.file_metadata import FileMetadata
3
+ from mfcli.models.pipeline_run import PipelineRun
4
+ from mfcli.utils.logger import get_logger
5
+ from mfcli.utils.orm import Session
6
+
7
+ logger = get_logger(__name__)
8
+
9
+
10
+ def create_file(db: Session, pipeline_run_id: int, metadata: FileMetadata) -> File:
11
+ logger.debug(f"Creating file: {metadata.name}")
12
+ existing_file: File | None = (
13
+ db.query(File)
14
+ .filter(File.md5 == metadata.md5)
15
+ .filter(File.pipeline_run_id == pipeline_run_id)
16
+ .one_or_none()
17
+ )
18
+ if existing_file:
19
+ raise ValueError(f"File {metadata.name} already exists and will not be processed")
20
+
21
+ pipeline_run: PipelineRun = (
22
+ db.query(PipelineRun)
23
+ .filter(PipelineRun.id == pipeline_run_id)
24
+ .one_or_none()
25
+ )
26
+ if not pipeline_run:
27
+ raise ValueError(f"Pipeline run ID does not exist: {pipeline_run_id}")
28
+
29
+ file = File(
30
+ name=metadata.name,
31
+ type=metadata.type_id,
32
+ mime_type=metadata.mime,
33
+ md5=metadata.md5,
34
+ path=str(metadata.path),
35
+ ext=metadata.ext,
36
+ is_datasheet=int(metadata.is_datasheet)
37
+ )
38
+ file.pipeline_run = pipeline_run
39
+ db.add(file)
40
+ db.flush()
41
+ logger.debug(f"File created: {file.id}")
42
+ return file
@@ -0,0 +1,26 @@
1
+ from typing import List
2
+
3
+ from mfcli.models.functional_blocks import FunctionalBlockMeta, FunctionalBlock, FunctionalBlockComponent
4
+ from mfcli.models.pipeline_run import PipelineRun
5
+ from mfcli.utils.orm import Session
6
+
7
+
8
+ def create_functional_blocks(
9
+ db: Session,
10
+ pipeline_run: PipelineRun,
11
+ blocks: List[FunctionalBlockMeta]
12
+ ) -> None:
13
+ db_blocks = []
14
+ for block in blocks:
15
+ db_block = FunctionalBlock(
16
+ name=block.name,
17
+ description=block.description,
18
+ pipeline_run=pipeline_run,
19
+ components=[]
20
+ )
21
+ for component_ref in (block.components or []):
22
+ db_block.components.append(
23
+ FunctionalBlockComponent(ref=component_ref)
24
+ )
25
+ db_blocks.append(db_block)
26
+ db.add_all(db_blocks)
mfcli/crud/netlist.py ADDED
@@ -0,0 +1,18 @@
1
+ from mfcli.models.netlist import NetlistSchema, Netlist, NetlistComponent, NetlistPin
2
+
3
+
4
+ def create_netlist(pipeline_run_id: int, netlist_schema: NetlistSchema) -> Netlist:
5
+ netlist = Netlist(pipeline_run_id=pipeline_run_id)
6
+ for component_schema in netlist_schema.components:
7
+ component = NetlistComponent(
8
+ ref_des=component_schema.ref_des,
9
+ part_number=component_schema.part_number
10
+ )
11
+ netlist.components.append(component)
12
+ for pin_schema in component_schema.pins:
13
+ pin = NetlistPin(
14
+ pin=pin_schema.pin,
15
+ net=pin_schema.net
16
+ )
17
+ component.pins.append(pin)
18
+ return netlist
@@ -0,0 +1,17 @@
1
+ from mfcli.models.pipeline_run import PipelineRun
2
+ from mfcli.models.project import Project
3
+ from mfcli.utils.logger import get_logger
4
+ from mfcli.utils.orm import Session
5
+
6
+ logger = get_logger(__name__)
7
+
8
+
9
+ def create_pipeline_run(db: Session, project: Project) -> PipelineRun:
10
+ logger.debug("Creating pipeline run")
11
+ run = PipelineRun()
12
+ run.project = project
13
+ db.add(run)
14
+ db.flush()
15
+ logger.debug(f"Pipeline run created: {run.id}")
16
+ db.commit()
17
+ return run
mfcli/crud/project.py ADDED
@@ -0,0 +1,99 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import List
7
+ from uuid import uuid4
8
+
9
+ from mfcli.utils.directory_manager import app_dirs
10
+ from pydantic import ValidationError
11
+
12
+ from mfcli.models.project import Project, project_name_regex
13
+ from mfcli.models.project_metadata import ProjectConfig
14
+ from mfcli.utils.logger import get_logger
15
+ from mfcli.utils.migrations import run_migrations
16
+ from mfcli.utils.orm import Session
17
+
18
+ project_name_const = "Project name length must be between 3 and 45 and have any of these characters: [A-Za-z0-9_-]"
19
+
20
+ logger = get_logger()
21
+
22
+
23
+ def create_project(db: Session, repo_dir: str, name: str) -> Project:
24
+ logger.debug(f"Creating project with name: {name}")
25
+ name = name.strip()
26
+ if not re.match(project_name_regex, name):
27
+ raise ValueError(project_name_const)
28
+ existing_project = db.query(Project).filter(Project.name == name).one_or_none()
29
+ if existing_project:
30
+ raise ValueError("A project with this name already exists")
31
+ project = Project(
32
+ name=name,
33
+ repo_dir=str(Path(repo_dir)),
34
+ index_id=uuid4().hex
35
+ )
36
+ db.add(project)
37
+ db.flush()
38
+ logger.debug(f"Project created: {project}")
39
+ return project
40
+
41
+
42
+ def get_project_by_name(db: Session, name: str) -> Project:
43
+ project: Project | None = (
44
+ db.query(Project)
45
+ .filter(Project.name == name)
46
+ .one_or_none()
47
+ )
48
+ if not project:
49
+ raise ValueError(f"A project by this name does not exist: {name}")
50
+ return project
51
+
52
+
53
+ def _list_projects(db: Session) -> List[Project]:
54
+ return db.query(Project).all()
55
+
56
+
57
+ def read_project_config_file() -> ProjectConfig:
58
+ file_path = app_dirs.config_file_path
59
+ logger.debug(f"Reading metadata file: {file_path}")
60
+ if not os.path.exists(file_path):
61
+ print('Could not find metadata file. Are you in the correct directory? Or if this is a new project, please initialize this repo with "mfcli init"')
62
+ sys.exit(1)
63
+ if not os.access(file_path, os.R_OK):
64
+ print(f"Could not access repo file: {file_path}")
65
+ sys.exit(1)
66
+ with open(file_path, "r") as f:
67
+ try:
68
+ return ProjectConfig(**json.loads(f.read()))
69
+ except ValidationError as e:
70
+ logger.error(f"Metadata file is corrupted: {file_path}")
71
+ raise e
72
+
73
+
74
+ def init_project(project_name: str | None, repo_dir: Path | None = None):
75
+ try:
76
+ with Session() as db:
77
+ run_migrations()
78
+ if not repo_dir:
79
+ repo_dir = Path(os.getcwd())
80
+ logger.debug(f"Initializing project for repo dir: {repo_dir}")
81
+ file_path = app_dirs.config_file_path
82
+ if os.path.exists(file_path):
83
+ project_config = read_project_config_file()
84
+ get_project_by_name(db, project_config.name)
85
+ logger.debug(f"Project has already been initialized: {project_config.name}")
86
+ return
87
+
88
+ repo_dir = str(repo_dir)
89
+ if not project_name:
90
+ project_name = input("Please choose a project name [A-Za-z0-9_-]: ")
91
+ create_project(db, repo_dir, project_name)
92
+ project_config = ProjectConfig(name=project_name)
93
+ logger.debug(f"Creating metadata file: {file_path}")
94
+ with open(file_path, "w") as f:
95
+ f.write(json.dumps(project_config.model_dump(), indent=2))
96
+ db.commit()
97
+ except Exception as e:
98
+ logger.exception(e)
99
+ logger.error("Error initializing project")
File without changes
@@ -0,0 +1,105 @@
1
+ import time
2
+
3
+ import requests
4
+ from requests import HTTPError
5
+
6
+ from mfcli.utils.config import get_config
7
+ from mfcli.utils.logger import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class DigiKey:
13
+ def __init__(self):
14
+ self._config = get_config()
15
+ self._url = "https://api.digikey.com"
16
+ self._token: str | None = None
17
+ self._get_access_token()
18
+ self._session = requests.Session()
19
+ headers = {
20
+ "Authorization": f"Bearer {self._token}",
21
+ "X-DIGIKEY-Client-Id": self._config.digikey_client_id,
22
+ "Content-Type": "application/json",
23
+ "Accept": "application/json"
24
+ }
25
+ self._session.headers = headers
26
+
27
+ @staticmethod
28
+ def _format_datasheet_url(url: str | None) -> str | None:
29
+ if url and url.startswith('//'):
30
+ return f"https:{url}"
31
+ return url
32
+
33
+ def _api_call(self, method: str, url: str, data: dict | None = None) -> dict:
34
+ max_retries = 5
35
+ timeout = 5
36
+ backoff_factor = 2
37
+
38
+ for attempt in range(1, max_retries + 1):
39
+ try:
40
+ resp = self._session.request(method=method, url=url, json=data, timeout=timeout)
41
+
42
+ if resp.status_code == 429:
43
+ if attempt == max_retries:
44
+ resp.raise_for_status()
45
+
46
+ retry_after = resp.headers.get("Retry-After")
47
+ delay = float(retry_after) if retry_after is not None else backoff_factor * (2 ** (attempt - 1))
48
+ time.sleep(delay)
49
+ continue
50
+
51
+ resp.raise_for_status()
52
+ return resp.json()
53
+
54
+ except requests.exceptions.Timeout:
55
+ # Handle timeout — retry with exponential backoff
56
+ if attempt == max_retries:
57
+ raise # give up after last attempt
58
+ delay = backoff_factor * (2 ** (attempt - 1))
59
+ time.sleep(delay)
60
+ continue
61
+
62
+ raise RuntimeError("Exhausted retries without success.")
63
+
64
+ def _get_access_token(self):
65
+ logger.debug("Fetching Digi-Key access token")
66
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
67
+ data = {
68
+ "client_id": self._config.digikey_client_id,
69
+ "client_secret": self._config.digikey_client_secret,
70
+ "grant_type": "client_credentials"
71
+ }
72
+ url = f"{self._url}/v1/oauth2/token"
73
+ resp = requests.post(url, headers=headers, data=data)
74
+ token_data = resp.json()
75
+ self._token = token_data["access_token"]
76
+
77
+ def _datasheet_from_part_number(self, part_number: str) -> str | None:
78
+ url = f"{self._url}/products/v4/search/{part_number}/productdetails"
79
+ resp = self._api_call('GET', url)
80
+ if not resp.get("Product"):
81
+ return None
82
+ return resp["Product"].get("DatasheetUrl")
83
+
84
+ def _keyword_search(self, part_number: str) -> str | None:
85
+ payload = {
86
+ "Keywords": part_number,
87
+ "RecordCount": 1
88
+ }
89
+ url = f"{self._url}/products/v4/search/keyword"
90
+ resp = self._api_call('POST', url, payload)
91
+ if not resp.get("ExactMatches"):
92
+ return None
93
+ return resp["ExactMatches"][0].get("DatasheetUrl")
94
+
95
+ def datasheet(self, part_number: str) -> str | None:
96
+ datasheet_url = None
97
+ try:
98
+ datasheet_url = self._datasheet_from_part_number(part_number)
99
+ except HTTPError as e:
100
+ # Handle 404 with keyword search API, otherwise raise HTTP error
101
+ if not e.response.status_code == 404:
102
+ raise e
103
+ if not datasheet_url:
104
+ datasheet_url = self._keyword_search(part_number)
105
+ return self._format_datasheet_url(datasheet_url)
mfcli/main.py ADDED
@@ -0,0 +1,5 @@
1
+ from mfcli.cli.main import run_cli
2
+
3
+
4
+ def main():
5
+ run_cli()
mfcli/mcp/__init__.py ADDED
File without changes
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "mfcli-mcp": {
4
+ "disabled": false,
5
+ "timeout": 60,
6
+ "type": "stdio",
7
+ "command": "python",
8
+ "args": ["-m", "mfcli.mcp.server"]
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "mcp_server": {
3
+ "name": "MFCLI MCP Server",
4
+ "command": "python",
5
+ "args": ["-m", "mfcli.mcp.server"]
6
+ }
7
+ }
@@ -0,0 +1,6 @@
1
+ from fastmcp import FastMCP
2
+
3
+ # Create the MCP instance that will be shared across modules
4
+ mcp = FastMCP(
5
+ name="ChromaDB Engineering Docs"
6
+ )
mfcli/mcp/server.py ADDED
@@ -0,0 +1,37 @@
1
+ import sys
2
+
3
+ import chromadb
4
+ from chromadb.utils import embedding_functions
5
+
6
+ from mfcli.mcp.mcp_instance import mcp
7
+ from mfcli.utils.config import get_config
8
+ from mfcli.utils.directory_manager import app_dirs
9
+
10
+ # Import tools to register them with mcp
11
+ import mfcli.mcp.tools.query_knowledgebase
12
+
13
+
14
+ def chroma_db_connection_test():
15
+ try:
16
+ config = get_config()
17
+ test_chroma_client = chromadb.PersistentClient(path=app_dirs.chroma_db_dir)
18
+ test_openai_ef = embedding_functions.OpenAIEmbeddingFunction(
19
+ api_key=config.openai_api_key,
20
+ model_name=config.embedding_model
21
+ )
22
+ test_chroma_client.get_or_create_collection("engineering_docs", embedding_function=test_openai_ef)
23
+ print(f"✓ Connected to ChromaDB at: {app_dirs.chroma_db_dir}", file=sys.stderr, flush=True)
24
+ print(f"✓ Collection: engineering_docs", file=sys.stderr, flush=True)
25
+ except Exception as e:
26
+ print(f"✗ Failed to connect to ChromaDB: {e}", file=sys.stderr, flush=True)
27
+ raise
28
+
29
+
30
+ def main():
31
+ """Entry point for mfcli-mcp command."""
32
+ chroma_db_connection_test()
33
+ mcp.run()
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -0,0 +1,51 @@
1
+ """State manager for MCP server to persist state between calls."""
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from mfcli.utils.directory_manager import app_dirs
7
+
8
+
9
+ class MCPStateManager:
10
+ """Manages persistent state for the MCP server."""
11
+
12
+ def __init__(self):
13
+ state_dir = Path(app_dirs.app_data_dir)
14
+ try:
15
+ state_dir.mkdir(parents=True, exist_ok=True)
16
+ except Exception:
17
+ # If we can't create the directory, we'll operate with in-memory state only.
18
+ pass
19
+ self.state_file = state_dir / "mcp_state.json"
20
+ self._state = self._load_state()
21
+
22
+ def _load_state(self) -> dict:
23
+ """Load state from disk."""
24
+ if self.state_file.exists():
25
+ try:
26
+ with open(self.state_file, 'r') as f:
27
+ return json.load(f)
28
+ except Exception:
29
+ return {}
30
+ return {}
31
+
32
+ def _save_state(self):
33
+ """Save state to disk."""
34
+ try:
35
+ with open(self.state_file, 'w') as f:
36
+ json.dump(self._state, f, indent=2)
37
+ except Exception:
38
+ pass # Silently fail if we can't save state
39
+
40
+ def get_last_project_name(self) -> Optional[str]:
41
+ """Get the last used project name."""
42
+ return self._state.get('last_project_name')
43
+
44
+ def set_last_project_name(self, project_name: str):
45
+ """Set the last used project name."""
46
+ self._state['last_project_name'] = project_name
47
+ self._save_state()
48
+
49
+
50
+ # Singleton instance
51
+ state_manager = MCPStateManager()
File without changes