Skip to content

Commit 27385a6

Browse files
committed
fix(docker): copy rest_provider.py into the image
The Dockerfile copies modules by explicit name, and rest_provider.py was never added when the REST/OAuth feature merged. The published image has the frontend/server code that imports rest_provider but not the module itself, so /api/pending-auth (polled by the UI) and any REST provider fail at runtime with ModuleNotFoundError. - Add rest_provider.py to the COPY line and set MCPPROXY_REST_AUTH_DIR. - Add tests/test_dockerfile.py guarding that every local module imported by runtime code is COPYd into the image, so a new module can't silently break the build again. https://claude.ai/code/session_01L9uGbkXi2RwUmBQHdVaNoZ
1 parent a1b592c commit 27385a6

2 files changed

Lines changed: 71 additions & 1 deletion

File tree

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ RUN pip install --no-cache-dir -r requirements.txt
1818
RUN pip install --no-cache-dir uv
1919
ENV PATH="/root/.local/bin:$PATH"
2020

21-
COPY server.py config.py process_runner.py builtin_tools.py tool_registry.py ./
21+
COPY server.py config.py process_runner.py builtin_tools.py tool_registry.py rest_provider.py ./
2222
COPY frontend/ ./frontend/
2323
COPY handlers/ ./handlers/
2424

@@ -29,6 +29,7 @@ ENV MCP_TOOL_CONFIG_DIR=/app/tools
2929
ENV MCP_ENV_FILE=/app/.env
3030
ENV MCPPROXY_FILES_DIR=/app/files
3131
ENV MCPPROXY_REPOS_DIR=/app/repos
32+
ENV MCPPROXY_REST_AUTH_DIR=/app/.rest-auth
3233

3334
EXPOSE 8888 8889
3435

tests/test_dockerfile.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Guard: every local module imported by runtime code must be COPYd into the image.
2+
3+
The Dockerfile copies Python modules by explicit name (not `COPY . .`), so adding
4+
a new top-level module that runtime code imports — without adding it to the COPY
5+
line — produces an image that imports a missing module at request time. This
6+
happened once with `rest_provider.py`; this test keeps it from recurring.
7+
"""
8+
import ast
9+
import re
10+
from pathlib import Path
11+
12+
ROOT = Path(__file__).resolve().parent.parent
13+
14+
15+
def _local_module_names() -> set[str]:
16+
"""Top-level .py files in the repo root are importable local modules."""
17+
return {p.stem for p in ROOT.glob("*.py")}
18+
19+
20+
def _copied_modules() -> set[str]:
21+
"""Module stems COPYd into the image by the Dockerfile (root-level .py files)."""
22+
text = (ROOT / "Dockerfile").read_text(encoding="utf-8")
23+
copied: set[str] = set()
24+
for line in text.splitlines():
25+
if line.startswith("COPY"):
26+
for tok in re.findall(r"(\w[\w./-]*\.py)\b", line):
27+
copied.add(Path(tok).stem)
28+
return copied
29+
30+
31+
def _imported_local_modules(py_files, local: set[str]) -> set[str]:
32+
used: set[str] = set()
33+
for path in py_files:
34+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
35+
for node in ast.walk(tree):
36+
if isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
37+
root = node.module.split(".")[0]
38+
if root in local:
39+
used.add(root)
40+
elif isinstance(node, ast.Import):
41+
for alias in node.names:
42+
root = alias.name.split(".")[0]
43+
if root in local:
44+
used.add(root)
45+
return used
46+
47+
48+
def test_runtime_imports_are_copied_into_image():
49+
local = _local_module_names()
50+
# Runtime entrypoints + the packages baked into the image.
51+
runtime_files = [ROOT / "server.py", ROOT / "frontend" / "app.py"]
52+
runtime_files += sorted((ROOT / "frontend").glob("*.py"))
53+
runtime_files += sorted((ROOT / "handlers").glob("*.py"))
54+
# Follow one hop: modules those entrypoints import are themselves copied and
55+
# may import further local modules.
56+
copied = _copied_modules()
57+
runtime_files += [ROOT / f"{m}.py" for m in copied if (ROOT / f"{m}.py").exists()]
58+
59+
used = _imported_local_modules(set(runtime_files), local)
60+
missing = used - copied
61+
assert not missing, (
62+
f"Local modules imported by runtime code but not COPYd in the Dockerfile: "
63+
f"{sorted(missing)}. Add them to the COPY line."
64+
)
65+
66+
67+
def test_rest_provider_is_in_the_image():
68+
# Explicit guard for the specific regression this test was written for.
69+
assert "rest_provider" in _copied_modules()

0 commit comments

Comments
 (0)