제미니 유료 결제를 하고 나서 이것저것 아이디어를 떠올리고 일부는 실행도 해봤다.
하다 보니 가장 신경 쓰이는 건 사고 못 치게 하는 것. 샌드박스 모드 기능은 준비되어 있다.
샌드박스 안에서는 내 허락을 구하지 않고 이것저것 다 할 수 있게 만들 수도 있다. 욜로 모드.
이 둘이 제대로 갖춰지면 충분히 잘 만들어진 기획서를 여러 md 파일들로 넘겨주고 몇 시간 동안 개입 없이 자동으로 돌리는 게 가능해진다.
여기까진 있는 기능 쓰는 거니까 문제가 없었다.
거기에 MCP를 켜서 이것저것 해보려는 시점부터 문제가 생긴다.
어디선가 Qdant와 Redis를 붙여서 장단기 기억을 확장하는 데 쓴다는 글의 제목만 얼핏 봐서 그거 나도 해보고 싶다고 생각했다. (나중에 아무리 찾아도 이거다 싶은 원문이 안 나오는 걸 봐서는 gemini cli 얘기가 아니었을 것 같기도 하다)
그런데 Redis MCP는 uvx로 실행되고, 샌드박스로 쓰이는 도커 컨테이너 안에는 기본적으로 uvx가 없어서 넣어줘야 한다.
https://geminicli.com/docs/get-started/configuration/
https://geminicli.com/docs/cli/sandbox/
https://geminicli.com/docs/tools/mcp-server/
공식 문서를 찾아보면 샌드박스 환경에 쓰이는 sandbox.Dockerfile 파일 같은 걸 만들어주면 된다고 적혀있긴 한데 문서는 '그냥 이렇게 하면 됩니다'까지만 있고 안 되면 어떻게 해결하라거나 디버그 로그를 보려면 어떻게 하라는 게 없다.
더구나 샌드박스 안에서 Redis로 접속하는 구조가 되다 보니 127.0.0.1로 해결이 안 되는데 이걸 Redis MCP에 변수값으로 줄려면 ~/.gemini/settings.json 파일 안에 mcpServers 블럭을 넣어줘야 하는데 이것도 정확히 어떤 형식을 따라야 하는지 딱 복붙 예시를 주는 데가 없다. '여기까지 왔으면 이런 건 다 알고 있을 테니까 굳이 얘기 안 한다?' 뭐 그런 건가.
제미니 유료 결제를 하고 나서 이것저것 하기 위한 전단계가 꽤 지친다.
---
오픈소스 동네의 암묵적인 룰에 따라, 결국 원본 gemini-cli 소스코드를 찾아봤는데, 아무래도 샌드박스 이미지를 직접 빌드하는 기능은 npm에는 빠져있고 소스코드 안에서만 동작하는 것 같다.
https://github.com/google-gemini/gemini-cli/issues/3404
EDIT: Looking into it, this appears to be intended behavior. If you want to use gemini to build the sandbox then you need to install from source.
빌드에 쓰이는 스크립트 파일이 있는데 이게 npm으로 설치한 파일 중에는 없어서 찾아보니 여러 중복 이슈들 끝에 저렇게 코멘트가 남겨진 이슈가 있다.
이래놓고 공식 문서 샌드박스 설명에는 이딴 식으로만 적어놨단 말이지 구글놈들
Missing commands
- Add to custom Dockerfile.
- Install via
sandbox.bashrc.
그야말로 '내 자리에선 되는데요'의 전형 아닌가
하다 하다 volta가 아직 덜 흔해서 문제일까 nvm으로 바꿔보기까지 했는데. 내 시간 물어내라 구글놈들아
---
깃허브 이슈의 코멘트에 적힌 '아예 공식 이미지 경로처럼 태그를 적어서 빌드해주면 된다'는 내용이 먹히긴 한다. (애초에 sandbox.Dockerfile FROM에 저걸 적어줘야 하는데 나중의 빌드에서는 순환참조가 되는 게 아닌가 싶지만)
~/.gemini$ docker build --no-cache --progress=plain -t us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.22.5 -f sandbox.Dockerfile .
이렇게 빌드하고 나서 gemini를 실행하면 redis mcp 에러 출력이 uvx 명령 없다는 것에서 접속 실패로 바뀌었다. spawn uvx ENOENT 대신 MCP error -32000: Connection closed
이제 settings.json 파일에 mcpServers 블럭을 붙여주면 끝이네 싶었지만, 그렇지 않았다.
sandbox.Dockerfile에 따로 redis-tools 패키지를 설치하고 gemini 안에서 !를 눌러서 redis-cli -h 172.17.0.1 info 도 해보고 redis-cli -h host.docker.internal info 도 되는 걸 확인했지만 여전히 mcp 초기화에서는 여전히 Connection closed가 뜬다. 그럼 docker나 redis 문제는 아니라는 건데 뭘 어떻게 해줘야 되는 거지?
https://redis.io/docs/latest/integrate/redis-mcp/client-conf/#claude-desktop
mcpServers 블럭 설정은 Redis MCP 공식 문서의 예시에서 type 키만 빼고 그대로 적용하니, gemini -s 실행 후에 에러가 안 나오고, /mcp schema 명령으로도 출력이 잘 나온다.
https://github.com/qdrant/mcp-server-qdrant
qdrant는 docker 실행하고 mcpServers에 추가해주는 것만으로 끝났다.
아니, 더 찾아보면 사실 제미니 자체는 이런 컨텍스트 관리를 외부로 확장하는 기능이 없으니 그렇게 쓰고 싶으면 ~/.gemini/GEMINI.md 같은 최상위 시방서에 잘 적어줘야 된다고 한다. 이 모험에서는 그냥 MCP 구성을 해봤다는 걸로 만족하기로 했다.
---
아래는 settings.json 파일 중 mcpServer 블럭을 그대로 복붙한 것. (from 인자를 안 주니까 디렉토리가 바뀌면 mcp가 해당 서버에 연결 못 하는 상황이 생김)
"mcpServers": {
"redis": {
"command": "uvx",
"args": [
"--from",
"redis-mcp-server@latest",
"redis-mcp-server",
"--url",
"redis://172.17.0.1:6379/0"
]
},
"qdrant": {
"command": "uvx",
"args": [
"--from",
"mcp-server-qdrant@latest",
"mcp-server-qdrant"
],
"env": {
"QDRANT_URL": "http://127.17.0.1:6333",
"COLLECTION_NAME": "gemini-cli",
"EMBEDDING_MODEL": "sentence-transformers/all-MiniLM-L6-v2"
}
}
}
---
기록을 위해 적어두자면 redis, qdrant는 docker로 실행해서 포트만 열어줌.
docker run --name redis-mcp -d -p 6379:6379 --restart=on-failure redis
docker run --name qdrant-mcp -d -p 6333:6333 --restart=on-failure qdrant/qdrant
redis-cli를 열어두고 gemini에서 적당한 명령을 내려보면 redis에 접속 기록이 남는 것을 확인할 수 있다.
172.17.0.1:6379> monitor
OK
1767225632.091199 [0 172.17.0.1:60296] "CLIENT" "SETINFO" "LIB-NAME" "redis-py(mcp-server_v0.4.1)"
1767225632.091377 [0 172.17.0.1:60296] "CLIENT" "SETINFO" "LIB-VER" "7.1.0"
1767225632.091540 [0 172.17.0.1:60296] "SCAN" "0" "MATCH" "session:*" "COUNT" "100"
1767225639.157522 [0 172.17.0.1:60296] "DBSIZE"
---
$ cat sandbox.Dockerfile
#FROM gemini-cli-sandbox
FROM us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.22.5 AS gemini-cli-sandbox
USER root
# uv를 모든 사용자가 접근 가능한 전역 경로에 설치i
RUN curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh
RUN uv --version
RUN uvx --version
RUN mkdir -p /tmp/uv_cache && chmod 777 /tmp/uv_cache
ENV UV_CACHE_DIR=/tmp/uv_cache
# Docker out of Docker method
#RUN apt-get update && apt-get install -y docker.io docker-compose # too old version
# TODO: copy from docker:dind image?
RUN apt-get update \
&& apt-get install -y ca-certificates curl \
&& install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \
&& chmod a+r /etc/apt/keyrings/docker.asc \
&& tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
RUN apt-get update
RUN apt search docker \
&& apt-get install -y docker-ce \
&& /bin/bash -c 'echo -e "{\n \"iptables\": false\n}" > /etc/docker/daemon.json'
# for qdrant mcp
RUN mkdir -p /tmp/fastembed_cache && chmod 777 /tmp/fastembed_cache
RUN apt-get install -y python3-pip
RUN pip3 install --break-system-packages fastembed
RUN HF_HOME=/tmp/hf_home FASTEMBED_CACHE_PATH=/tmp/fastembed_cache python3 -c "from fastembed import TextEmbedding; import os; TextEmbedding('sentence-transformers/all-MiniLM-L6-v2')"
RUN npm install -g pyright #typescript typescript-language-server
USER gemini
FROM을 정확히 어떻게 적어줘야 하는지는 여전히 잘 모르겠다. 제미니 공식 문서에도 딱 깔끔하게 안내해주지는 않음. 공식 문서에서 안내하는 FROM gemini-cli-sandbox는 실제로는 빌드가 되지 않는 걸 봐선 저것도 소스 저장소 기반에서만 동작하는 걸 그대로 문서에 적어둔 게 아닐까 의심.
docker 설치는 docker out of docker라는 방식으로 호스트의 docker에 접속하기 위함. 프로젝트 자체가 docker-compose를 사용하면서 sandbox가 호스트의 docker에 접속하지 못하면 디버깅을 위해 제미니가 뭔가 실행해서 결과를 보려고 할 때 직접 할 수 없어서 사용자더러 실행하고 복붙해달라고 요청하게 된다. 무중단 실행을 위해서는 꼭 필요한 접근이다. 물론 보안성이 좀 떨어지긴 하지만 제미니 외에 다른 컨테이너 자체가 없다면 문제도 없을 것.
qdrant 쪽은 처음에는 됐던 걸로 기억하는데 왜인지 어느 순간부터는 qdrant에 접속 못한다고 -32000 에러를 냈다. mcpServers의 명령행을 gemini 안에서 쉘로 직접 실행해보니 모델이 없다고 떠서 아예 sandbox.Dockerfile 안에서 미리 파일을 받도록 했다. 이게 확실한 해결책인지는 아니면 내부 쉘에서 수동으로 뭔가 한 게 효과를 낸 건지 잘은 모르겠지만 어쨌든 진행 과정중에는 에러가 없어진 걸 확인했다.
lsp MCP는 serena라는 걸 써보려는 참. 작업하다가 변수 이름이나 인자 이름 같은 걸 꽤 자주 틀리고 쉘 스크립트 문법 같은 것도 틀려서 고치고 다시 실행하는 걸 자주 봐서 LSP 역할이 필요하긴 하구나 싶다. 찾아보니 깃허브 이슈에 남아있어서 아직 정식 지원은 아닌가 싶기도 하지만 일단 /mcp list 목록에는 드니까. -> lsp를 활성화한 뒤에 꽤 규모 있는 여기저기 다 바뀌는 작업을 시켜봤는데 막 코드 짜다가 아 이 필드는 사실 다른 이름인데? 아 이 데이타는 없는데 로직상 필요하네? 이러면서 스크롤을 이어가는 모습이 보인다. lsp 효과인지 아닌지는 잘 모르겠다. 어디를 봐야 lsp를 써먹고 있는지 알 수 있나? -> "replace_content (lsp MCP Server)" 라는 블럭이 지나가는 걸 봐서는 진짜 써먹고 있긴 하다. 붙여주면 도움이 되긴 하는구나. 근데 MCP가 토큰 많이 쓴다는 얘기를 봤던 거 같아서 약간 신경 쓰임.
gemini라는 계정은 실제로는 없음. USER를 따로 지정하지 않은 상태로 RUN으로 뭔가 권한 없을만한 명령을 실행하면 gemini라는 계정이 없다고 에러 나는 걸 확인했던 것으로 기억.
샌드박스 컨테이너에 docker inspect 해보면 실시간으로 그룹과 계정을 추가하는 것처럼 보임. export SANDBOX_SET_UID_GID=false 플래그를 켜면 그런 계정이 passwd 파일에 없다고 샌드박스 자체가 안 뜨는 장면을 볼 수 있음. 기본적으로는 true로 동작하는 결과로 추가하는 것으로 보임.
https://github.com/google-gemini/gemini-cli/blob/8f0324d868904bece4d370d06d5a3ed275ae9ade/packages/cli/src/utils/sandboxUtils.ts#L39
https://github.com/google-gemini/gemini-cli/blob/8f0324d868904bece4d370d06d5a3ed275ae9ade/packages/cli/src/utils/sandbox.ts#L574
소스코드를 보면 true도 false도 아니면서 데비안이나 우분투면 (아마 최상위 FROM 자체가 여기에 기반하고 있어서) 호스트 UID, GID를 넘기는 게 기본이고, 그 외에는 false로 처리하는 방식.
계정과 그룹을 실시간으로 추가하기 때문에 docker run --group-add 식으로 추가 그룹을 설정해도 먹히지 않는 걸로 보이는데 docker out of docker 방식을 위해 호스트의 groups 중에 하나를 샌드박스 안쪽까지 전달하려면?
좋지 않은 방법이지만 일단 호스트에서 sudo chmod o+rw /var/run/docker.sock 처리해서 샌드박스에서 docker ps가 소켓을 읽을 수 있는 건 확인했다.