lucidscan 0.5.12__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 (91) hide show
  1. lucidscan/__init__.py +12 -0
  2. lucidscan/bootstrap/__init__.py +26 -0
  3. lucidscan/bootstrap/paths.py +160 -0
  4. lucidscan/bootstrap/platform.py +111 -0
  5. lucidscan/bootstrap/validation.py +76 -0
  6. lucidscan/bootstrap/versions.py +119 -0
  7. lucidscan/cli/__init__.py +50 -0
  8. lucidscan/cli/__main__.py +8 -0
  9. lucidscan/cli/arguments.py +405 -0
  10. lucidscan/cli/commands/__init__.py +64 -0
  11. lucidscan/cli/commands/autoconfigure.py +294 -0
  12. lucidscan/cli/commands/help.py +69 -0
  13. lucidscan/cli/commands/init.py +656 -0
  14. lucidscan/cli/commands/list_scanners.py +59 -0
  15. lucidscan/cli/commands/scan.py +307 -0
  16. lucidscan/cli/commands/serve.py +142 -0
  17. lucidscan/cli/commands/status.py +84 -0
  18. lucidscan/cli/commands/validate.py +105 -0
  19. lucidscan/cli/config_bridge.py +152 -0
  20. lucidscan/cli/exit_codes.py +17 -0
  21. lucidscan/cli/runner.py +284 -0
  22. lucidscan/config/__init__.py +29 -0
  23. lucidscan/config/ignore.py +178 -0
  24. lucidscan/config/loader.py +431 -0
  25. lucidscan/config/models.py +316 -0
  26. lucidscan/config/validation.py +645 -0
  27. lucidscan/core/__init__.py +3 -0
  28. lucidscan/core/domain_runner.py +463 -0
  29. lucidscan/core/git.py +174 -0
  30. lucidscan/core/logging.py +34 -0
  31. lucidscan/core/models.py +207 -0
  32. lucidscan/core/streaming.py +340 -0
  33. lucidscan/core/subprocess_runner.py +164 -0
  34. lucidscan/detection/__init__.py +21 -0
  35. lucidscan/detection/detector.py +154 -0
  36. lucidscan/detection/frameworks.py +270 -0
  37. lucidscan/detection/languages.py +328 -0
  38. lucidscan/detection/tools.py +229 -0
  39. lucidscan/generation/__init__.py +15 -0
  40. lucidscan/generation/config_generator.py +275 -0
  41. lucidscan/generation/package_installer.py +330 -0
  42. lucidscan/mcp/__init__.py +20 -0
  43. lucidscan/mcp/formatter.py +510 -0
  44. lucidscan/mcp/server.py +297 -0
  45. lucidscan/mcp/tools.py +1049 -0
  46. lucidscan/mcp/watcher.py +237 -0
  47. lucidscan/pipeline/__init__.py +17 -0
  48. lucidscan/pipeline/executor.py +187 -0
  49. lucidscan/pipeline/parallel.py +181 -0
  50. lucidscan/plugins/__init__.py +40 -0
  51. lucidscan/plugins/coverage/__init__.py +28 -0
  52. lucidscan/plugins/coverage/base.py +160 -0
  53. lucidscan/plugins/coverage/coverage_py.py +454 -0
  54. lucidscan/plugins/coverage/istanbul.py +411 -0
  55. lucidscan/plugins/discovery.py +107 -0
  56. lucidscan/plugins/enrichers/__init__.py +61 -0
  57. lucidscan/plugins/enrichers/base.py +63 -0
  58. lucidscan/plugins/linters/__init__.py +26 -0
  59. lucidscan/plugins/linters/base.py +125 -0
  60. lucidscan/plugins/linters/biome.py +448 -0
  61. lucidscan/plugins/linters/checkstyle.py +393 -0
  62. lucidscan/plugins/linters/eslint.py +368 -0
  63. lucidscan/plugins/linters/ruff.py +498 -0
  64. lucidscan/plugins/reporters/__init__.py +45 -0
  65. lucidscan/plugins/reporters/base.py +30 -0
  66. lucidscan/plugins/reporters/json_reporter.py +79 -0
  67. lucidscan/plugins/reporters/sarif_reporter.py +303 -0
  68. lucidscan/plugins/reporters/summary_reporter.py +61 -0
  69. lucidscan/plugins/reporters/table_reporter.py +81 -0
  70. lucidscan/plugins/scanners/__init__.py +57 -0
  71. lucidscan/plugins/scanners/base.py +60 -0
  72. lucidscan/plugins/scanners/checkov.py +484 -0
  73. lucidscan/plugins/scanners/opengrep.py +464 -0
  74. lucidscan/plugins/scanners/trivy.py +492 -0
  75. lucidscan/plugins/test_runners/__init__.py +27 -0
  76. lucidscan/plugins/test_runners/base.py +111 -0
  77. lucidscan/plugins/test_runners/jest.py +381 -0
  78. lucidscan/plugins/test_runners/karma.py +481 -0
  79. lucidscan/plugins/test_runners/playwright.py +434 -0
  80. lucidscan/plugins/test_runners/pytest.py +598 -0
  81. lucidscan/plugins/type_checkers/__init__.py +27 -0
  82. lucidscan/plugins/type_checkers/base.py +106 -0
  83. lucidscan/plugins/type_checkers/mypy.py +355 -0
  84. lucidscan/plugins/type_checkers/pyright.py +313 -0
  85. lucidscan/plugins/type_checkers/typescript.py +280 -0
  86. lucidscan-0.5.12.dist-info/METADATA +242 -0
  87. lucidscan-0.5.12.dist-info/RECORD +91 -0
  88. lucidscan-0.5.12.dist-info/WHEEL +5 -0
  89. lucidscan-0.5.12.dist-info/entry_points.txt +34 -0
  90. lucidscan-0.5.12.dist-info/licenses/LICENSE +201 -0
  91. lucidscan-0.5.12.dist-info/top_level.txt +1 -0
@@ -0,0 +1,275 @@
1
+ """Configuration file generator.
2
+
3
+ Generates lucidscan.yml configuration files based on detected project
4
+ characteristics and user choices.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ import yaml
14
+
15
+ from lucidscan.detection import ProjectContext
16
+
17
+
18
+ @dataclass
19
+ class InitChoices:
20
+ """User choices made during initialization."""
21
+
22
+ # Linting
23
+ linter: Optional[str] = None # "ruff", "eslint", "biome", or None
24
+ linter_config: Optional[str] = None # Path to existing config or None
25
+
26
+ # Type checking
27
+ type_checker: Optional[str] = None # "mypy", "pyright", "typescript", or None
28
+ type_checker_strict: bool = False
29
+
30
+ # Security
31
+ security_enabled: bool = True
32
+ security_tools: list[str] = field(default_factory=lambda: ["trivy", "opengrep"])
33
+
34
+ # Testing
35
+ test_runner: Optional[str] = None # "pytest", "jest", or None
36
+ coverage_enabled: bool = False
37
+ coverage_threshold: int = 80
38
+
39
+ # Fail thresholds
40
+ fail_on_linting: str = "error" # "error", "warning", "none"
41
+ fail_on_security: str = "high" # "critical", "high", "medium", "low", "none"
42
+
43
+
44
+ class ConfigGenerator:
45
+ """Generates lucidscan.yml configuration files."""
46
+
47
+ def generate(
48
+ self,
49
+ context: ProjectContext,
50
+ choices: InitChoices,
51
+ ) -> str:
52
+ """Generate lucidscan.yml content.
53
+
54
+ Args:
55
+ context: Detected project context.
56
+ choices: User initialization choices.
57
+
58
+ Returns:
59
+ YAML string for lucidscan.yml.
60
+ """
61
+ config = self._build_config(context, choices)
62
+ return self._to_yaml(config)
63
+
64
+ def write(
65
+ self,
66
+ context: ProjectContext,
67
+ choices: InitChoices,
68
+ output_path: Optional[Path] = None,
69
+ ) -> Path:
70
+ """Generate and write lucidscan.yml file.
71
+
72
+ Args:
73
+ context: Detected project context.
74
+ choices: User initialization choices.
75
+ output_path: Output file path (default: project_root/lucidscan.yml).
76
+
77
+ Returns:
78
+ Path to the written file.
79
+ """
80
+ if output_path is None:
81
+ output_path = context.root / "lucidscan.yml"
82
+
83
+ content = self.generate(context, choices)
84
+ output_path.write_text(content)
85
+ return output_path
86
+
87
+ def _build_config(
88
+ self,
89
+ context: ProjectContext,
90
+ choices: InitChoices,
91
+ ) -> dict:
92
+ """Build configuration dictionary.
93
+
94
+ Args:
95
+ context: Detected project context.
96
+ choices: User initialization choices.
97
+
98
+ Returns:
99
+ Configuration dictionary.
100
+ """
101
+ config = {
102
+ "version": 1,
103
+ "project": self._build_project_section(context),
104
+ }
105
+
106
+ # Add pipeline section
107
+ pipeline = {}
108
+
109
+ # Linting
110
+ if choices.linter:
111
+ pipeline["linting"] = self._build_linting_section(choices)
112
+
113
+ # Type checking
114
+ if choices.type_checker:
115
+ pipeline["type_checking"] = self._build_type_checking_section(choices)
116
+
117
+ # Security
118
+ if choices.security_enabled and choices.security_tools:
119
+ pipeline["security"] = self._build_security_section(choices)
120
+
121
+ # Testing
122
+ if choices.test_runner:
123
+ pipeline["testing"] = self._build_testing_section(choices)
124
+
125
+ # Coverage
126
+ if choices.coverage_enabled:
127
+ pipeline["coverage"] = self._build_coverage_section(choices)
128
+
129
+ if pipeline:
130
+ config["pipeline"] = pipeline
131
+
132
+ # Fail thresholds
133
+ config["fail_on"] = self._build_fail_on_section(choices)
134
+
135
+ # Ignore patterns
136
+ config["ignore"] = self._build_ignore_patterns(context)
137
+
138
+ return config
139
+
140
+ def _build_project_section(self, context: ProjectContext) -> dict:
141
+ """Build project section."""
142
+ languages = [lang.name for lang in context.languages]
143
+ return {
144
+ "name": context.root.name,
145
+ "languages": languages,
146
+ }
147
+
148
+ def _build_linting_section(self, choices: InitChoices) -> dict:
149
+ """Build linting pipeline section."""
150
+ tools: list[dict] = []
151
+ section = {
152
+ "enabled": True,
153
+ "tools": tools,
154
+ }
155
+
156
+ tool: dict = {"name": choices.linter}
157
+ if choices.linter_config:
158
+ tool["config"] = choices.linter_config
159
+
160
+ tools.append(tool)
161
+ return section
162
+
163
+ def _build_type_checking_section(self, choices: InitChoices) -> dict:
164
+ """Build type checking pipeline section."""
165
+ tools: list[dict] = []
166
+ section = {
167
+ "enabled": True,
168
+ "tools": tools,
169
+ }
170
+
171
+ tool: dict = {"name": choices.type_checker}
172
+ if choices.type_checker_strict:
173
+ tool["strict"] = True
174
+
175
+ tools.append(tool)
176
+ return section
177
+
178
+ def _build_security_section(self, choices: InitChoices) -> dict:
179
+ """Build security pipeline section."""
180
+ tools: list[dict] = []
181
+ section = {
182
+ "enabled": True,
183
+ "tools": tools,
184
+ }
185
+
186
+ for tool_name in choices.security_tools:
187
+ tool: dict = {"name": tool_name}
188
+
189
+ # Add domains based on tool
190
+ if tool_name == "trivy":
191
+ tool["domains"] = ["sca"]
192
+ elif tool_name == "opengrep":
193
+ tool["domains"] = ["sast"]
194
+ elif tool_name == "checkov":
195
+ tool["domains"] = ["iac"]
196
+
197
+ tools.append(tool)
198
+
199
+ return section
200
+
201
+ def _build_testing_section(self, choices: InitChoices) -> dict:
202
+ """Build testing pipeline section."""
203
+ section = {
204
+ "enabled": True,
205
+ "tools": [{"name": choices.test_runner}],
206
+ }
207
+ return section
208
+
209
+ def _build_coverage_section(self, choices: InitChoices) -> dict:
210
+ """Build coverage pipeline section."""
211
+ return {
212
+ "enabled": True,
213
+ "threshold": choices.coverage_threshold,
214
+ }
215
+
216
+ def _build_fail_on_section(self, choices: InitChoices) -> dict:
217
+ """Build fail_on thresholds section."""
218
+ return {
219
+ "linting": choices.fail_on_linting,
220
+ "type_checking": "error",
221
+ "security": choices.fail_on_security,
222
+ "testing": "any",
223
+ "coverage": "below_threshold" if choices.coverage_enabled else "none",
224
+ }
225
+
226
+ def _build_ignore_patterns(self, context: ProjectContext) -> list:
227
+ """Build ignore patterns list."""
228
+ patterns = [
229
+ "**/__pycache__/**",
230
+ "**/node_modules/**",
231
+ "**/.venv/**",
232
+ "**/venv/**",
233
+ "**/dist/**",
234
+ "**/build/**",
235
+ "**/.git/**",
236
+ ]
237
+
238
+ # Add language-specific patterns
239
+ if context.has_python:
240
+ patterns.extend([
241
+ "**/*.egg-info/**",
242
+ "**/.pytest_cache/**",
243
+ "**/.mypy_cache/**",
244
+ "**/.ruff_cache/**",
245
+ ])
246
+
247
+ if context.has_javascript:
248
+ patterns.extend([
249
+ "**/coverage/**",
250
+ "**/.next/**",
251
+ "**/.nuxt/**",
252
+ ])
253
+
254
+ return patterns
255
+
256
+ def _to_yaml(self, config: dict) -> str:
257
+ """Convert config dict to YAML string.
258
+
259
+ Args:
260
+ config: Configuration dictionary.
261
+
262
+ Returns:
263
+ YAML string.
264
+ """
265
+ # Add header comment
266
+ header = "# LucidScan Configuration\n# Generated by lucidscan init\n\n"
267
+
268
+ yaml_content = yaml.dump(
269
+ config,
270
+ default_flow_style=False,
271
+ sort_keys=False,
272
+ allow_unicode=True,
273
+ )
274
+
275
+ return header + yaml_content
@@ -0,0 +1,330 @@
1
+ """Package manager installer.
2
+
3
+ Adds development tools to package manager configuration files
4
+ (pyproject.toml, package.json).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import re
11
+ from pathlib import Path
12
+ from typing import Dict, List
13
+
14
+ from lucidscan.core.logging import get_logger
15
+ from lucidscan.detection import ProjectContext
16
+
17
+ LOGGER = get_logger(__name__)
18
+
19
+ # Tool to package mapping
20
+ PYTHON_PACKAGES: Dict[str, str] = {
21
+ "ruff": "ruff>=0.8.0",
22
+ "mypy": "mypy>=1.0",
23
+ "pyright": "pyright>=1.1",
24
+ "pytest": "pytest>=7.0",
25
+ "pytest-cov": "pytest-cov>=4.0",
26
+ }
27
+
28
+ JAVASCRIPT_PACKAGES: Dict[str, str] = {
29
+ "eslint": "eslint@^9.0.0",
30
+ "biome": "@biomejs/biome@^1.0.0",
31
+ "typescript": "typescript@^5.0.0",
32
+ "jest": "jest@^29.0.0",
33
+ "vitest": "vitest@^2.0.0",
34
+ }
35
+
36
+
37
+ class PackageInstaller:
38
+ """Adds tools to package manager configuration files."""
39
+
40
+ def install_tools(
41
+ self,
42
+ context: ProjectContext,
43
+ tools: List[str],
44
+ ) -> Dict[str, Path]:
45
+ """Install tools to appropriate package files.
46
+
47
+ Args:
48
+ context: Detected project context.
49
+ tools: List of tool names to install.
50
+
51
+ Returns:
52
+ Dict mapping tool name to the file it was added to.
53
+ """
54
+ installed: Dict[str, Path] = {}
55
+
56
+ # Separate tools by language
57
+ python_tools = [t for t in tools if t in PYTHON_PACKAGES]
58
+ js_tools = [t for t in tools if t in JAVASCRIPT_PACKAGES]
59
+
60
+ # Install Python tools
61
+ if python_tools and context.has_python:
62
+ pyproject = context.root / "pyproject.toml"
63
+ requirements = context.root / "requirements-dev.txt"
64
+
65
+ if pyproject.exists():
66
+ added = self._add_to_pyproject(pyproject, python_tools)
67
+ for tool in added:
68
+ installed[tool] = pyproject
69
+ elif requirements.exists():
70
+ added = self._add_to_requirements(requirements, python_tools)
71
+ for tool in added:
72
+ installed[tool] = requirements
73
+ else:
74
+ # Create pyproject.toml if no package file exists
75
+ added = self._create_pyproject(context.root, python_tools)
76
+ for tool in added:
77
+ installed[tool] = context.root / "pyproject.toml"
78
+
79
+ # Install JavaScript tools
80
+ if js_tools and context.has_javascript:
81
+ package_json = context.root / "package.json"
82
+
83
+ if package_json.exists():
84
+ added = self._add_to_package_json(package_json, js_tools)
85
+ for tool in added:
86
+ installed[tool] = package_json
87
+ else:
88
+ # Create package.json if it doesn't exist
89
+ added = self._create_package_json(context.root, js_tools)
90
+ for tool in added:
91
+ installed[tool] = context.root / "package.json"
92
+
93
+ return installed
94
+
95
+ def _add_to_pyproject(
96
+ self,
97
+ pyproject_path: Path,
98
+ tools: List[str],
99
+ ) -> List[str]:
100
+ """Add tools to pyproject.toml dev dependencies.
101
+
102
+ Args:
103
+ pyproject_path: Path to pyproject.toml.
104
+ tools: Tools to add.
105
+
106
+ Returns:
107
+ List of tools that were added.
108
+ """
109
+ content = pyproject_path.read_text()
110
+ added = []
111
+
112
+ # Check which tools are already present
113
+ packages_to_add = []
114
+ for tool in tools:
115
+ package = PYTHON_PACKAGES.get(tool)
116
+ if package:
117
+ # Check if already in file (simple check)
118
+ tool_name = package.split(">=")[0].split("[")[0]
119
+ if tool_name not in content:
120
+ packages_to_add.append(package)
121
+ added.append(tool)
122
+
123
+ if not packages_to_add:
124
+ return added
125
+
126
+ # Check if [project.optional-dependencies] exists
127
+ if "[project.optional-dependencies]" in content:
128
+ # Check if dev section exists
129
+ if re.search(r'\[project\.optional-dependencies\].*?dev\s*=', content, re.DOTALL):
130
+ # Add to existing dev list
131
+ content = self._append_to_dev_deps(content, packages_to_add)
132
+ else:
133
+ # Add dev section after [project.optional-dependencies]
134
+ dev_section = f'\ndev = [\n {self._format_deps(packages_to_add)}\n]'
135
+ content = content.replace(
136
+ "[project.optional-dependencies]",
137
+ f"[project.optional-dependencies]{dev_section}"
138
+ )
139
+ elif "[project]" in content:
140
+ # Add optional-dependencies section
141
+ deps_section = f'\n[project.optional-dependencies]\ndev = [\n {self._format_deps(packages_to_add)}\n]\n'
142
+ # Find a good place to insert (after dependencies if exists, or at end)
143
+ if "dependencies = [" in content:
144
+ # Find end of dependencies section
145
+ match = re.search(r'dependencies\s*=\s*\[.*?\]', content, re.DOTALL)
146
+ if match:
147
+ insert_pos = match.end()
148
+ content = content[:insert_pos] + deps_section + content[insert_pos:]
149
+ else:
150
+ # Add at end of [project] section
151
+ content += deps_section
152
+ else:
153
+ # No project section, add one
154
+ content += f'\n[project.optional-dependencies]\ndev = [\n {self._format_deps(packages_to_add)}\n]\n'
155
+
156
+ pyproject_path.write_text(content)
157
+ LOGGER.info(f"Added {len(added)} tools to {pyproject_path}")
158
+ return added
159
+
160
+ def _append_to_dev_deps(self, content: str, packages: List[str]) -> str:
161
+ """Append packages to existing dev dependencies list."""
162
+ # Find the dev = [...] section and append
163
+ pattern = r'(dev\s*=\s*\[)(.*?)(\])'
164
+
165
+ def replacer(match):
166
+ existing = match.group(2).strip()
167
+ if existing and not existing.endswith(","):
168
+ existing += ","
169
+ new_deps = self._format_deps(packages)
170
+ return f'{match.group(1)}{existing}\n {new_deps}\n{match.group(3)}'
171
+
172
+ return re.sub(pattern, replacer, content, flags=re.DOTALL)
173
+
174
+ def _format_deps(self, packages: List[str]) -> str:
175
+ """Format packages as TOML array items."""
176
+ return ",\n ".join(f'"{pkg}"' for pkg in packages)
177
+
178
+ def _add_to_requirements(
179
+ self,
180
+ requirements_path: Path,
181
+ tools: List[str],
182
+ ) -> List[str]:
183
+ """Add tools to requirements-dev.txt.
184
+
185
+ Args:
186
+ requirements_path: Path to requirements file.
187
+ tools: Tools to add.
188
+
189
+ Returns:
190
+ List of tools that were added.
191
+ """
192
+ content = requirements_path.read_text()
193
+ added = []
194
+
195
+ for tool in tools:
196
+ package = PYTHON_PACKAGES.get(tool)
197
+ if package:
198
+ tool_name = package.split(">=")[0]
199
+ if tool_name not in content:
200
+ content += f"\n{package}"
201
+ added.append(tool)
202
+
203
+ if added:
204
+ requirements_path.write_text(content.strip() + "\n")
205
+ LOGGER.info(f"Added {len(added)} tools to {requirements_path}")
206
+
207
+ return added
208
+
209
+ def _create_pyproject(
210
+ self,
211
+ project_root: Path,
212
+ tools: List[str],
213
+ ) -> List[str]:
214
+ """Create pyproject.toml with dev dependencies.
215
+
216
+ Args:
217
+ project_root: Project root directory.
218
+ tools: Tools to add.
219
+
220
+ Returns:
221
+ List of tools that were added.
222
+ """
223
+ packages = [PYTHON_PACKAGES[t] for t in tools if t in PYTHON_PACKAGES]
224
+
225
+ content = f'''[project]
226
+ name = "{project_root.name}"
227
+ version = "0.3.0"
228
+ requires-python = ">=3.10"
229
+
230
+ [project.optional-dependencies]
231
+ dev = [
232
+ {self._format_deps(packages)}
233
+ ]
234
+
235
+ [build-system]
236
+ requires = ["setuptools>=64"]
237
+ build-backend = "setuptools.build_meta"
238
+ '''
239
+
240
+ pyproject_path = project_root / "pyproject.toml"
241
+ pyproject_path.write_text(content)
242
+ LOGGER.info(f"Created {pyproject_path}")
243
+ return tools
244
+
245
+ def _add_to_package_json(
246
+ self,
247
+ package_json_path: Path,
248
+ tools: List[str],
249
+ ) -> List[str]:
250
+ """Add tools to package.json devDependencies.
251
+
252
+ Args:
253
+ package_json_path: Path to package.json.
254
+ tools: Tools to add.
255
+
256
+ Returns:
257
+ List of tools that were added.
258
+ """
259
+ content = package_json_path.read_text()
260
+ data = json.loads(content)
261
+ added = []
262
+
263
+ if "devDependencies" not in data:
264
+ data["devDependencies"] = {}
265
+
266
+ for tool in tools:
267
+ package = JAVASCRIPT_PACKAGES.get(tool)
268
+ if package:
269
+ # Parse package name and version
270
+ if "@" in package and not package.startswith("@"):
271
+ name, version = package.rsplit("@", 1)
272
+ elif package.startswith("@"):
273
+ # Scoped package like @biomejs/biome@^1.0.0
274
+ parts = package.split("@")
275
+ name = f"@{parts[1]}"
276
+ version = parts[2] if len(parts) > 2 else "latest"
277
+ else:
278
+ name = package
279
+ version = "latest"
280
+
281
+ if name not in data["devDependencies"]:
282
+ data["devDependencies"][name] = version
283
+ added.append(tool)
284
+
285
+ if added:
286
+ package_json_path.write_text(
287
+ json.dumps(data, indent=2) + "\n"
288
+ )
289
+ LOGGER.info(f"Added {len(added)} tools to {package_json_path}")
290
+
291
+ return added
292
+
293
+ def _create_package_json(
294
+ self,
295
+ project_root: Path,
296
+ tools: List[str],
297
+ ) -> List[str]:
298
+ """Create package.json with devDependencies.
299
+
300
+ Args:
301
+ project_root: Project root directory.
302
+ tools: Tools to add.
303
+
304
+ Returns:
305
+ List of tools that were added.
306
+ """
307
+ data = {
308
+ "name": project_root.name,
309
+ "version": "1.0.0",
310
+ "devDependencies": {},
311
+ }
312
+
313
+ for tool in tools:
314
+ package = JAVASCRIPT_PACKAGES.get(tool)
315
+ if package:
316
+ if "@" in package and not package.startswith("@"):
317
+ name, version = package.rsplit("@", 1)
318
+ elif package.startswith("@"):
319
+ parts = package.split("@")
320
+ name = f"@{parts[1]}"
321
+ version = parts[2] if len(parts) > 2 else "latest"
322
+ else:
323
+ name = package
324
+ version = "latest"
325
+ data["devDependencies"][name] = version # type: ignore[index]
326
+
327
+ package_json_path = project_root / "package.json"
328
+ package_json_path.write_text(json.dumps(data, indent=2) + "\n")
329
+ LOGGER.info(f"Created {package_json_path}")
330
+ return tools
@@ -0,0 +1,20 @@
1
+ """MCP (Model Context Protocol) integration for LucidScan.
2
+
3
+ This package provides MCP server functionality for AI agent integration,
4
+ enabling tools like Claude Code and Cursor to invoke LucidScan checks.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from lucidscan.mcp.server import LucidScanMCPServer
10
+ from lucidscan.mcp.formatter import InstructionFormatter, FixInstruction
11
+ from lucidscan.mcp.tools import MCPToolExecutor
12
+ from lucidscan.mcp.watcher import LucidScanFileWatcher
13
+
14
+ __all__ = [
15
+ "LucidScanMCPServer",
16
+ "InstructionFormatter",
17
+ "FixInstruction",
18
+ "MCPToolExecutor",
19
+ "LucidScanFileWatcher",
20
+ ]