Subprocess Scan¶
lcp.subprocess_scan ¶
Run package scans in a separate interpreter.
The MCP server must not import arbitrary package code in-process: import-time
side effects can crash the server, heavy imports stall concurrent tool calls
(CPython import lock + GIL, even though FastMCP runs sync tools on worker
threads), and the server's environment is often not the project's environment.
This module launches an isolated child interpreter that runs only raw
introspection and parses the ScannedModule tree it writes to stdout as
JSON; the host then runs :func:lcp.generator.generate_lcp with its own
pydantic to produce the LCP document.
The child is started as <python> -c <bootstrap> <package>. The bootstrap
loads the self-contained scanner.py and the stdlib-only _childscan.py
by absolute file path (so lcp/__init__.py — and thus pydantic and
fastmcp — never runs in the child) and adds only extra_paths to the
child's sys.path. The host site-packages is therefore never exposed:
target submodules cannot import host packages, and the target's
pydantic_core cannot collide with the host's pydantic. The target
interpreter needs neither lcp nor pydantic installed. The child
inherits the server's working directory; with -c, that directory is also
on its sys.path, mirroring in-process import behavior.
Residual trust model (documented, not solved here): the scanned package's import-time code still executes — just in a disposable child process. Process isolation contains crashes and hangs; it is not a sandbox.
DEFAULT_SCAN_TIMEOUT
module-attribute
¶
Seconds before a scan subprocess is killed (heavy imports can be slow).
SubprocessScanError ¶
ScanImportError ¶
Bases: SubprocessScanError
The target package is not importable in the scan environment (exit 3).
ScanFailedError ¶
Bases: SubprocessScanError
The scan ran but failed: crash, invalid output, or unexpected exit.
ScanTimeoutError ¶
Bases: SubprocessScanError
The scan subprocess exceeded its timeout and was killed.
ScanInterpreterNotFoundError ¶
Bases: SubprocessScanError
The configured scan interpreter does not exist.
ScanSpawnError ¶
Bases: SubprocessScanError
The scan subprocess could not be started (process spawning restricted).
ScanResult
dataclass
¶
A generated LCP document plus the diagnostics from producing it.
Attributes:
| Name | Type | Description |
|---|---|---|
document |
LCPDocument
|
The generated LCP document. |
unresolved_reexports |
list[tuple[str, int, int]]
|
|
Source code in src/lcp/subprocess_scan.py
scan_package_subprocess ¶
scan_package_subprocess(package: str, python: str | None = None, timeout: float = DEFAULT_SCAN_TIMEOUT, extra_paths: Sequence[str] = ()) -> ScanResult
Scan package in a child interpreter and return its LCP document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
package
|
str
|
Import path of the package to scan (e.g. |
required |
python
|
str | None
|
Interpreter whose environment gets scanned. Defaults to
|
None
|
timeout
|
float
|
Seconds before the child is killed. |
DEFAULT_SCAN_TIMEOUT
|
extra_paths
|
Sequence[str]
|
Additional |
()
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
ScanResult
|
class: |
ScanResult
|
generated on the host from the child's raw |
|
ScanResult
|
plus any unresolved re-export diagnostics from the scan. |
Raises:
| Type | Description |
|---|---|
ScanImportError
|
The package is not importable in the scan environment. |
ScanFailedError
|
The scan crashed or produced invalid output. |
ScanTimeoutError
|
The child exceeded timeout and was killed. |
ScanInterpreterNotFoundError
|
python does not exist. |
ScanSpawnError
|
The child process could not be spawned at all. |
Source code in src/lcp/subprocess_scan.py
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | |
lcp.scanjson ¶
Machine-mode scan entry point: python -m lcp.scanjson <package>.
Runs inside the scan interpreter — possibly a different venv from the MCP
server — and speaks the public LCP document format on stdout, so there is no
private IPC format to version. Errors are a single JSON object
{"type": ..., "message": ...} on stderr plus a distinguishing exit code.
Exit codes
0: success — the LCP JSON document is on stdout.
3: import failure — the target package could not be imported.
4: scan failure — the package imported but scanning or generation failed,
including SystemExit raised by import-time code.
Deliberately imports only the stdlib until arguments are parsed, and never
imports click: the scan environment only needs lcp and pydantic
importable (the MCP server appends its own paths to the child's sys.path
for exactly that — see :mod:lcp.subprocess_scan).
main ¶
Scan a package and write its LCP document to stdout.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
argv
|
list[str] | None
|
Argument list (default: |
None
|