Gemini CLI: 커스텀 도구(Custom Tool) 직접 추가하기
단일 기능을 수행하는 custom tool을
Gemini CLI에 직접 등록하고 사용하는 방법을 설명함.
Gemini 0.33.1 기준임.
참고로 현재 Gemini CLI는 스킬(Skill)을 통해 여러 기능을 묶은 패키지 형태의 확장도 지원함.
이번 튜토리얼의 목표는 다음과 같음.
- 프로젝트 루트에
.gemini/settings.json을 둠 - 기존
tools설정, 특히shell설정을 유지함 - 여기에
discoveryCommand와callCommand를 추가함 - Python 스크립트로 custom tool을 구현함
- Gemini CLI에서 실제로 사용해봄
1. 프로젝트 구조
예제 프로젝트 구조는 다음과 같음.
my-project/
├── .gemini/
│ └── settings.json
└── bin/
├── get_tools.py
├── call_tool.py
└── hello_tool.py
각 파일의 역할은 다음과 같음.
.gemini/settings.json- 현재 프로젝트에서만 적용되는 Gemini CLI 설정 파일
bin/get_tools.py- Gemini CLI에 "어떤 custom tool들이 있는지" 알려주는 discovery 스크립트
bin/call_tool.py- Gemini CLI가 특정 tool을 호출할 때 실행하는 dispatcher
bin/hello_tool.py- 실제 도구 로직
프로젝트 설정 파일 .gemini/settings.json은
- 현재 프로젝트에서 Gemini CLI를 실행할 때만 적용되며,
- 사용자 전역 설정보다 우선함.
2. 프로젝트 전용 settings.json 작성
먼저 프로젝트 루트에 .gemini/settings.json을 만듦.
- 중요한 점은
tools가 배열이 아니라 객체라는 점임. - 따라서
shell,discoveryCommand,callCommand를 모두 하나의 tools 객체 안에 함께 적어야 함. tools.discoveryCommand,tools.callCommand,tools.allowed,tools.exclude,tools.shell.*는 모두 같은 설정 섹션에 속함.
다음을 주의할 것:
- 프로젝트 전용:
.gemini/settings.json(추천)- 프로젝트 루트 밑에 .gemini 디렉토리 생성할 것.)
- 전역 사용:
~/.gemini/settings.json
예제는 다음과 같음1:
{
"tools": {
"discoveryCommand": "python bin/get_tools.py",
"callCommand": "python bin/call_tool.py",
"shell": {
"enableInteractiveShell": true,
"pager": "cat",
"showColor": false
}
}
}
discoveryCommand- 사용 가능한 custom tool 목록을 반환하는 명령
callCommand- 선택된 tool을 실제로 실행하는 명령
shell.enableInteractiveShell- interactive shell 사용 여부
shell.pager- shell 출력 pager.
- 기본값은
cat이므로 사실 생략 가능함 (추천).
여기서 중요한 것은, 기존에 shell 설정이 이미 있었다면 그것을 지우지 말고 같은 tools 객체 안에 합쳐야 한다는 점임.
3. 단계별 구축 가이드
1단계: 실행할 스크립트 준비 (hello_tool.py)
복잡한 로직은 별도 스크립트로 만드는 것이 안전함.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
'say_hello' 도구의 실제 구현 스크립트임.
stdin으로 JSON 데이터를 받아 'name' 값을 추출하고,
이름이 포함된 환영 메시지를 JSON 형식으로 만들어 stdout으로 출력함.
"""
import sys
import json
def main():
"""
스크립트의 메인 로직을 실행함.
"""
try:
# stdin에서 JSON 데이터를 읽어 파싱함.
input_data = json.load(sys.stdin)
except:
# JSON 파싱 실패 시(예: 입력 없음) 빈 딕셔너리로 초기화함.
input_data = {}
# 입력 데이터에서 'name' 키의 값을 가져옴. 키가 없으면 'World'를 기본값으로 사용함.
name = input_data.get("name", "World")
# 최종 결과물인 환영 메시지를 JSON 객체로 만듦.
output_message = {
"message": f"Hello, {name}! Welcome to Gemini CLI Tools."
}
# 결과 JSON 객체를 문자열로 변환하여 stdout으로 출력함.
print(json.dumps(output_message))
if __name__ == "__main__":
main()
- 프로젝트 루트 밑에 bin 디렉토리를 만들고 추가
- 이 스크립트는 표준 입력으로 JSON 인자를 받고, 표준 출력으로 JSON 결과를 반환함.
위의 경우처럼 shebang을 사용할 경우엔, 실행권한을 주는 것을 권함.
2단계: discovery 스크립트 작성
이제 Gemini CLI가 어떤 custom tool을 쓸 수 있는지 알려주는 bin/get_tools.py를 작성:
# -*- coding: utf-8 -*-
# bin/get_tools.py
"""
Gemini CLI에 사용 가능한 도구의 명세(specification)를 정의하고 출력하는 스크립트임.
이 스크립트는 JSON 형식으로 도구 목록을 stdout으로 출력함.
Gemini CLI는 이 출력을 파싱하여 사용자 정의 도구를 발견(discover)하고 등록함.
"""
import json
def main():
"""
사용자 정의 도구의 명세를 리스트 형태로 정의함.
"""
# 도구 명세 정의. 각 도구는 이름, 설명, 파라미터 등으로 구성됨.
tools = [
{
"name": "say_hello",
"description": "주어진 이름에게 인사말을 반환함.",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "인사할 대상 이름"
}
},
"required": ["name"]
}
}
]
# 도구 명세 리스트를 JSON 문자열로 변환하여 출력함.
# ensure_ascii=False 옵션은 한글이 깨지지 않도록 보장함.
print(json.dumps(tools, ensure_ascii=False))
if __name__ == "__main__":
main()
- Gemini CLI는
tools.discoveryCommand를 실행하여 tool schema 목록을 등록함. - 공식 설정 문서와 Tools API 문서는에 따라 discovery command가 JSON으로 도구 설명을 출력해야 함.
3단계: call dispatcher 작성
공식 문서에 따르면 tools.callCommand는
- 첫 번째 인자로 tool 이름을 받고,
stdin으로 JSON 인자를 읽고,stdout으로 JSON 결과를 출력해야 함.
따라서 여러 도구를 확장 가능하게 만들려면 dispatcher 형태가 적합함.
예제는 다음과 같음:
# -*- coding: utf-8 -*-
# bin/call_tool.py
"""
Gemini CLI 사용자 정의 도구를 호출하기 위한 진입점 스크립트임.
명령줄 인수로 도구 이름을 받고, stdin으로 해당 도구의 인수를 JSON으로 받음.
지정된 도구를 서브프로세스로 실행하고 결과를 stdout으로 반환함.
"""
import json
import subprocess
import sys
def run_python_tool(script_path: str, args: dict) -> None:
"""
지정된 파이썬 스크립트를 서브프로세스로 실행하고 결과를 처리함.
"""
process = subprocess.Popen(
[sys.executable, script_path],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# JSON 인수를 서브프로세스의 stdin으로 전달하고, stdout과 stderr를 받아옴.
stdout, stderr = process.communicate(
input=json.dumps(args, ensure_ascii=False)
)
# 서브프로세스 실행 실패 시, 에러 메시지를 출력하고 종료함.
if process.returncode != 0:
raise SystemExit(stderr.strip() or "Tool execution failed.")
# 실행 성공 시, 결과값을 stdout으로 출력함.
print(stdout.strip())
def main():
"""
스크립트의 메인 로직을 실행함.
"""
# 첫 번째 인수로 도구 이름이 주어졌는지 확인. 없으면 에러 발생.
if len(sys.argv) < 2:
raise SystemExit("Tool name is required as the first argument.")
tool_name = sys.argv[1]
# stdin에서 JSON 데이터를 읽어 인수로 사용함.
args = json.load(sys.stdin)
# 도구 이름이 'say_hello'인 경우의 처리.
if tool_name == "say_hello":
run_python_tool("bin/hello_tool.py", args)
return
# 목록에 없는 도구일 경우 에러 발생.
raise SystemExit(f"Unknown tool: {tool_name}")
if __name__ == "__main__":
main()
- Gemini CLI가
python bin/call_tool.py say_hello형태로 실행함 stdin으로{"name": "길동"}같은 JSON이 전달됨- dispatcher가
say_hello를 보고hello_tool.py에 전달함 - 최종 JSON 결과를 stdout으로 출력함
2025.06.07 - [Python] - [Py] subprocess 모듈 사용법.
[Py] subprocess 모듈 사용법.
이 문서는 subprocess Module 의 HighLevel Methods의 사용법을 다룸:subprocess.run(),subprocess.getoutput(),subprocess.check_output(),subprocess.PopenPython 3.5 이상을 기준으로 작성됨.1. subprocess 모듈이란?subprocess 모듈은 Pytho
ds31x.tistory.com
4단계: Gemini CLI에서 사용하기
이제 프로젝트 루트에서 Gemini CLI를 실행함.
- 이때
.gemini/settings.json이 자동으로 적용됨. - 프로젝트 설정은 현재 프로젝트에서만 적용되며, 사용자 전역 설정보다 우선 적용됨.
도구가 로드되었는지 확인하려면 /tools를 사용할 수 있음.
- 도구 문서는 custom tools와 MCP tools가 함께 registry에 등록된다고 설명함.
예를 들어 Gemini CLI에서 다음처럼 요청할 수 있음.
say_hello 도구를 사용해서 "길동"에게 인사해줘.
Gemini CLI는 내부적으로 다음 순서로 동작함.
discoveryCommand로say_hello도구를 등록함- 사용자 요청을 보고
say_hello를 선택함 callCommand를say_hello라는 첫 번째 인자와 함께 실행함- JSON 인자를 stdin으로 전달함
- JSON 결과를 받아 최종 답변을 구성함
5단계: Custom Tool 추가해보기
다음의 지정된 디렉토리의 disk usage를 체크하는 python script를 custom tool로 추가
import json
import os
import sys
from pathlib import Path
def format_bytes(num: int) -> str:
units = ["B", "KB", "MB", "GB", "TB", "PB"]
value = float(num)
for unit in units:
if value < 1024 or unit == units[-1]:
return f"{value:.2f} {unit}"
value /= 1024
def get_directory_size(path: Path) -> int:
total = 0
for root, dirs, files in os.walk(path, onerror=None, followlinks=False):
for name in files:
file_path = Path(root) / name
try:
if not file_path.is_symlink():
total += file_path.stat().st_size
except (FileNotFoundError, PermissionError, OSError):
pass
return total
def main():
args = json.load(sys.stdin)
path_str = args.get("path", ".")
path = Path(path_str).resolve()
if not path.exists():
raise SystemExit(f"Path does not exist: {path}")
if path.is_file():
try:
size = path.stat().st_size
except (PermissionError, OSError) as e:
raise SystemExit(str(e))
result = {
"path": str(path),
"type": "file",
"size_bytes": size,
"size_human": format_bytes(size)
}
print(json.dumps(result, ensure_ascii=False))
return
if not path.is_dir():
raise SystemExit(f"Not a regular file or directory: {path}")
size = get_directory_size(path)
result = {
"path": str(path),
"type": "directory",
"size_bytes": size,
"size_human": format_bytes(size)
}
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
이 python sciprt는 다음을 JSON으로 반환함.
- path: 계산한 대상 경로
- type: file 또는 directory
- size_bytes: 실제 바이트 수
- size_human: 사람이 읽기 쉬운 단위 문자열
이를 찾을 수 있도록 discovery 스크립트를 다음과 같이 변경:
# -*- coding: utf-8 -*-
# bin/get_tools.py
"""
Gemini CLI에 사용 가능한 도구의 명세(specification)를 정의하고 출력하는 스크립트임.
이 스크립트는 JSON 형식으로 도구 목록을 stdout으로 출력함.
Gemini CLI는 이 출력을 파싱하여 사용자 정의 도구를 발견(discover)하고 등록함.
"""
import json
def main():
"""
사용자 정의 도구의 명세를 리스트 형태로 정의함.
"""
# 도구 명세 정의. 각 도구는 이름, 설명, 파라미터 등으로 구성됨.
tools = [
{
"name": "say_hello",
"description": "주어진 이름에게 인사말을 반환함.",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "인사할 대상 이름"
}
},
"required": ["name"]
}
},
{
"name": "check_size",
"description": "지정한 디렉터리와 그 하위 파일들의 총 크기를 반환함.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "크기를 확인할 디렉터리 경로",
"default": "."
}
}
}
}
]
# 도구 명세 리스트를 JSON 문자열로 변환하여 출력함.
# ensure_ascii=False 옵션은 한글이 깨지지 않도록 보장함.
print(json.dumps(tools, ensure_ascii=False))
if __name__ == "__main__":
main()
이제 call dispatcher가 이들 두 tools를 모두 분기 처리해야 함.
import json
import subprocess
import sys
def run_python_tool(script_path: str, args: dict) -> None:
"""
지정된 파이썬 스크립트를 서브프로세스로 실행하고 결과를 처리함.
"""
process = subprocess.Popen(
[sys.executable, script_path],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# JSON 인수를 서브프로세스의 stdin으로 전달하고, stdout과 stderr를 받아옴.
stdout, stderr = process.communicate(
input=json.dumps(args, ensure_ascii=False)
)
# 서브프로세스 실행 실패 시, 에러 메시지를 출력하고 종료함.
if process.returncode != 0:
raise SystemExit(stderr.strip() or "Tool execution failed.")
# 실행 성공 시, 결과값을 stdout으로 출력함.
print(stdout.strip())
def main():
if len(sys.argv) < 2:
raise SystemExit("Tool name is required as the first argument.")
tool_name = sys.argv[1]
args = json.load(sys.stdin)
if tool_name == "say_hello":
run_python_tool("bin/hello_tool.py", args)
return
# 다음이 추가된 부분임!
if tool_name == "check_size":
run_python_tool("bin/disk_usage_tool.py", args)
return
raise SystemExit(f"Unknown tool: {tool_name}")
if __name__ == "__main__":
main()
callCommand는
- tool 이름을 첫 번째 인자로 받고,
- JSON 인자를 stdin으로 받아 실행 결과를 stdout으로 반환
프로젝트 루트에서 Gemini CLI를 실행한 뒤 /tools로 등록 여부를 확인할 수 있음.
/tools는 현재 활성화된 도구 목록을 보여주고, /tools desc는 전체 설명을 보여줌.
custom tools와 MCP tools가 제대로 로드되었는지 점검할 때 사용됨.

예시 요청은 다음과 같음.
check_size 도구를 사용해서 현재 폴더의 디스크 사용량을 알려줘.
3. Custom Tools vs. Skills 비교
| 상황 | 추천 방식 | 활성화 방식 |
| 단일 명령 실행 또는 단순 로컬 기능 | Custom Tool | 설정 시 자동 발견 |
| 프로젝트 전용 유틸리티 | Custom Tool | 설정 시 자동 발견 |
| 복잡한 절차, 지침, 리소스를 함께 묶는 경우 | Skill | 필요 시 on-demand 활성화 |
| 재사용 가능한 패키지 형태로 팀과 공유하는 경우 | Skill 권장 | 설치 후 필요 시 활성화 |
Custom Tool은
settings.json의tools.discoveryCommand와tools.callCommand를 통해 등록되는 실행형 도구임.
- 설정되어 있으면 Gemini CLI가 세션 시작 시 도구를 발견하여 registry에 올리므로, 별도 skill activation 절차 없이 사용할 수 있음.
Skill은
- 단순 실행 도구라기보다,
- 전문 지식, 절차, 스크립트, 리소스를 묶어 두는 on-demand capability에 가까움.
- Gemini CLI는 세션 시작 시 skill의 이름과 설명만 먼저 인식하고,
- 요청과 맞는 skill이 있으면 activate_skill을 통해 해당 skill의 본문과 리소스를 불러옴.
- 즉, "필요 시 활성화" 또는 "on-demand 활성화" 가 이루어짐.
"타인과 도구 공유"는
- Skill 쪽이 더 적합하지만, Custom Tool은 아예 공유가 불가능한 것은 아님.
- Custom Tool도 프로젝트의
.gemini/settings.json과 관련 스크립트를 저장소에 함께 넣으면 팀 단위 공유는 가능함. - 다만 Skill은
gemini skills install,link,enable,disable같은 관리 체계가 별도로 있어서 재사용 가능한 패키지로 배포하고 관리하기에 더 적합함.
4. 주의사항
커스텀 도구를 만들 때는 다음을 주의하는 것이 좋음.
- 결과는 JSON으로 출력할 것
- 예외 처리와 stderr 처리를 넣을 것
- shell 설정이 이미 있다면 tools 객체 안에 병합할 것
- 외부 명령을 감싸는 경우 보안에 주의할 것
- 또한
shell.pager는 필수가 아니고 기본값이cat임: 다만 예제처럼 명시해 두면 설정 의도가 더 분명해짐.
같이보면 좋은 자료들
- https://geminicli.com/docs/reference/configuration/
Gemini CLI의 설정 계층, settings.json 위치, 각 설정 키와 기본값을 정리한 설정 기준 문서임. - https://geminicli.com/docs/reference/tools/
Gemini CLI가 제공하는 built-in tools와 도구 사용 방식, 확인 방법을 정리한 도구 참조 문서임. - https://geminicli.com/docs/cli/skills/
Agent Skill의 개념, 구조, 활성화 방식, 관리 명령을 설명하는 Skill 안내 문서임. - https://geminicli.com/docs/tools/mcp-server/
Gemini CLI에서 MCP 서버를 설정하고 연결하여 외부 도구와 리소스를 사용하는 방법을 설명하는 문서임.
2026.03.14 - [utils] - Gemini CLI: tools, skills and MCP
'utils' 카테고리의 다른 글
| vim 9.2 릴리즈 (0) | 2026.02.18 |
|---|---|
| vscode - coding shortcuts (0) | 2026.02.18 |
| 연습 - Vim Regular Expression (0) | 2026.02.18 |
| Vim Regular Expression (0) | 2026.02.18 |
| [vim] buffer 와 window (0) | 2026.01.24 |