opencode-mcp
# opencode-mcp
이 프로젝트는 Anthropic이나 OpenCode 프로젝트와 관련이 없으며 보증을 받지 않았습니다. Claude Code와 OpenCode는 각 소유자의 상표입니다.
**기업용 Claude Code가 내부 OpenCode에 코딩 작업을 위임하고 결과를 이어서 확인하도록 하는 stdio MCP 서버입니다.** OpenAI Codex의 `codex` / `codex-reply` MCP 흐름과 비슷한 인터페이스를 제공합니다. Codex의 `codex mcp-server` 진입점은 Codex 0.154.0에서 제거됐습니다.
## 무엇을 해결하는가
인터넷 제한 환경에서 Claude Code의 작업을 사내 LLM gateway에 연결된 OpenCode에 전달하고, 결과 확인·추가 지시·취소·정리를 한 MCP 세션에서 처리합니다.
## 동작 구조
```text
Claude Code ⇄ stdio ⇄ opencode-mcp ⇄ HTTP ⇄ opencode serve ⇄ internal LLM gateway
│
managed 또는 attach
```
- **managed**(기본): opencode-mcp가 로컬 `opencode serve`를 시작하고 관리합니다. `OPENCODE_MCP_AIRGAP=1`이면 미설정된 일부 OpenCode 네트워크 기본값을 제한합니다.
- **attach**: 기존 OpenCode 서버에 연결합니다. `OPENCODE_MCP_SERVER_URL`을 설정하면 기본 모드도 `attach`입니다.
## 설치와 사용 (단계별)
처음 설치한다면 아래 순서로 준비하고 연결합니다. 인터넷 제한 환경의 패키지 미러링, TLS 및 조직 배포는 [에어갭 배포 체크리스트](docs/deployment-airgap.md)를 참고하세요.
### 0) 준비물
- Node.js 20 이상 (`package.json`의 `engines` 기준)
- OpenCode 1.18.33 (`opencode-ai`): [배포 체크리스트 §2](docs/deployment-airgap.md#2-opencode-설치-및-검증)의 설치·버전 확인 절차를 따릅니다.
- `rg`(ripgrep)가 서비스 계정 `PATH`에 있어야 합니다. `rg`가 없으면 OpenCode가 ripgrep을 내려받으려 할 수 있습니다(인터넷 제한 환경에서는 실패).
- Claude Code. git은 작업 저장소와 변경 확인에 권장합니다.
### 1) OpenCode 모델 설정
**OpenCode 설정에서 사내 provider만 켜져 있고(`enabled_providers`, 또는 `disabled_providers`로 외부 provider 차단) `model`·`small_model`이 사내 모델로 지정되어 있다면 provider를 새로 구성할 필요는 없습니다(아래 "처음 설정한다면" 부분은 건너뛰세요). 다만 아래 확인 항목은 점검하세요 — 그리고 이는 요청이 그대로 동작한다는 뜻일 뿐, 각 모델에 `limit`을 지정하지 않았다면 OpenCode는 그 모델의 context 크기를 0으로 보고하고 선제적으로 압축(compaction)하지 않으므로, context 인지 작업 배분을 쓰려면 아래 "모델 context 크기를 알려주고 동시 실행을 제어하려면" 안내도 함께 확인하세요.** managed 모드의 opencode-mcp는 Claude Code를 실행한 사용자 권한으로 `opencode serve`를 직접 띄우고, 이 프로세스는 평소 쓰던 OpenCode 설정(전역·관리형·프로젝트 `opencode.json`, `OPENCODE_CONFIG`)과 OpenCode에 저장된 인증 정보를 사용합니다. 단, 아래 환경 변수 전달 규칙과 에어갭 기본값이 함께 적용됩니다([배포 체크리스트 §5](docs/deployment-airgap.md#5-managedattach-모드)). attach 모드(`OPENCODE_MCP_SERVER_URL`)라면 연결한 서버의 설정을 쓰므로 이 단계는 그 서버 관리자가 맡습니다(모델은 `OPENCODE_MCP_DEFAULT_MODEL`로 고를 수 있습니다).
작업할 저장소에서 `opencode debug config`로 설정을 먼저 확인하세요. 이 명령은 셸 환경 기준이며, 출력에 API 키가 평문으로 나올 수 있으니 공유하지 마세요. TUI에서 모델을 고른 것만으로는 서버의 기본 모델이 되지 않을 수 있고, agent 설정에 `model`이 있으면 설정의 `model`보다 그것이 우선합니다(opencode-mcp가 모델을 보내면 보낸 모델이 우선). `OPENCODE_MCP_DEFAULT_MODEL`을 지정하지 않으면 opencode-mcp는 모델을 보내지 않고 OpenCode의 기본값을 따릅니다. 주 모델을 고정하고 결과에 모델 이름을 표시하려면 `OPENCODE_MCP_DEFAULT_MODEL=<provider>/<model>`을 지정하세요(외부 provider 차단은 `enabled_providers`가 맡습니다). 그 밖에 다음 경우를 확인하세요.
- **API 키를 환경 변수로 넘기는 경우**: opencode-mcp는 Claude Code가 넘겨준 환경만 받습니다. 셸에서는 보이는 키가 IDE나 데스크톱 앱에서 실행한 Claude Code에는 없을 수 있습니다. 이때는 키를 OpenCode에 저장하거나(`opencode providers login`, 별칭 `opencode auth login`; 저장한 키를 쓴다면 provider 설정의 `apiKey` 항목은 빼세요) `claude mcp add`의 `--env`로 전달하세요. 두 방법 모두 키가 파일(OpenCode 사용자 데이터, Claude Code MCP 설정)에 평문으로 남으므로 조직 비밀 관리 정책을 확인하고, 조직 배포는 [배포 체크리스트 §6](docs/deployment-airgap.md#6-claude-code-관리-등록)의 방식을 따르세요. 키를 git에 커밋되는 프로젝트 `.mcp.json`에 적지는 마세요.
- **조직이 `CLAUDE_CODE_MCP_ALLOWLIST_ENV=1`을 쓰는 경우**: Claude Code는 기본 환경(`HOME`, `PATH`, 일부 `CLAUDE*`)과 서버 `env`만 opencode-mcp에 넘깁니다. 키 변수와, 쓰고 있다면 `OPENCODE_CONFIG`·`OPENCODE_CONFIG_CONTENT`, 사용하는 `XDG_*` 변수, 사내 CA·프록시 변수를 각각 이름으로 서버 `env`에 넣으세요([배포 체크리스트 §6](docs/deployment-airgap.md#6-claude-code-관리-등록)).
- **변수 이름이 `ANTHROPIC_`, `CLAUDE_` 등으로 시작하는 경우**: Claude Code 자신의 인증 정보가 넘어가지 않도록 opencode-mcp는 `ANTHROPIC_*`, `CLAUDE_*`, `OPENCODE_MCP_*`, `OPENCODE_SERVER_*`, `CLAUDECODE`, `AI_AGENT`를 OpenCode에 전달하지 않으며, 아래 허용 목록에 넣어도 전달되지 않습니다. 사내 gateway의 URL이나 키를 이런 이름(예: `ANTHROPIC_BASE_URL`, `ANTHROPIC_API_KEY`)으로 쓰고 있었다면 URL은 OpenCode 설정의 `provider.<id>.options.baseURL`에 적고, 키는 다른 이름의 변수(예: `INTERNAL_LLM_API_KEY`)로 옮겨 provider 설정의 `apiKey`에서 `{env:INTERNAL_LLM_API_KEY}`로 참조하거나 OpenCode에 저장하세요.
- **`OPENCODE_MCP_CHILD_ENV_ALLOWLIST`를 설정한 경우**: 목록의 변수와 기본 변수(`PATH`, `HOME`, `USER`, `LOGNAME`, `LANG`, `LC_ALL`, `TMPDIR`, `SHELL`, `TERM`)만 전달됩니다. 키 변수와, 쓰고 있다면 `OPENCODE_*`(`OPENCODE_CONFIG`, `OPENCODE_CONFIG_CONTENT` 등), `XDG_*`, 사내 CA·프록시 변수(`NODE_EXTRA_CA_CERTS`, `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`)도 목록에 넣으세요(`PREFIX_*` 형식 지원).
- **설정이 특정 저장소의 `opencode.json`에만 있는 경우**: 그 저장소가 작업 디렉터리일 때 적용됩니다. `cwd`를 주지 않으면 `OPENCODE_MCP_DEFAULT_CWD`, 없으면 `CLAUDE_PROJECT_DIR`(Claude Code 프로젝트 디렉터리), 그것도 없으면 opencode-mcp 프로세스의 현재 디렉터리가 기준입니다. `OPENCODE_DISABLE_PROJECT_CONFIG=1`이면 프로젝트 설정은 무시됩니다.
적용 여부는 [4) 동작 확인](#4-동작-확인)에서 모델 목록으로 확인합니다.
**모델 context 크기를 알려주고 동시 실행을 제어하려면** ([모델별 컨텍스트에 맞춘 작업 배분](#모델별-컨텍스트에-맞춘-작업-배분), [병렬 실행과 최대 실행 개수](#병렬-실행과-최대-실행-개수) 참고) 다음을 권장합니다.
- **OpenCode 쪽에 `limit`을 지정하세요 (1차 해결책).** 사내 모델에 `limit: {context, output}`(필요하면 `input`도)을 지정하지 않으면 `GET /provider`는 그 모델을 `{"limit":{"context":0,"output":0}}`으로 보고합니다. 이 상태에서는 OpenCode가 그 모델에 선제적 compaction(요약)을 전혀 하지 않고 매 요청마다 `max_tokens`를 32000으로 보냅니다(gateway가 더 작은 상한만 허용하면 거부될 수 있습니다). `limit`을 지정하면 OpenCode 자신의 선제적 compaction이 켜지고 올바른 `max_tokens`가 전송됩니다 — 아래 설정 예의 `limit` 필드를 참고하세요.
- **`enabled_providers`를 쓰세요.** 지정하지 않으면 OpenCode 내장 models.dev catalog 전체(`GET /provider` 약 6 MiB, provider 226개)가 그대로 응답에 담겨 모든 turn과 `opencode-info section:"models"` 호출이 느려질 수 있습니다. `enabled_providers: ["corp"]`만 켜면 응답이 몇 KB로 줄어듭니다.
- `limit`을 넣을 수 없거나 아직 넣지 않았다면, opencode-mcp는 그 모델의 context 크기를 전혀 모르는 상태이므로 `OPENCODE_MCP_MODEL_PROFILES`로 알려줘야 context 인지 작업 배분이 가능합니다(이 경우 `limitSource`는 `"profile"`로 표시되고, OpenCode 자신은 여전히 compaction하지 않습니다).
- **`OPENCODE_MCP_DEFAULT_MODEL`을 지정하세요.** 모델 없이 보낸 turn은 어떤 모델이 실제로 쓰일지 opencode-mcp가 미리 알 수 없으므로, prompt 크기 가드(`OPENCODE_MCP_CONTEXT_GUARD`)와 모델별 동시 실행 cap(`OPENCODE_MCP_MODEL_PROFILES`의 `maxRunning`)의 대상이 되지 않고 전역 cap만 적용됩니다.
처음 설정한다면 OpenCode가 사용할 모델 provider를 설정합니다. 설정하지 않으면 OpenCode 기본 provider가 외부 클라우드일 수 있으므로, 사내 gateway 사용 시 먼저 provider를 구성하세요. [배포 체크리스트 §3](docs/deployment-airgap.md#3-내부-openai-compatible-gateway)의 OpenAI-compatible 설정은 다음 형태입니다(실제 gateway URL, model ID, key 환경 변수로 바꾸세요).
```json
{
"enabled_providers": ["corp"],
"provider": {
"corp": {
"npm": "@ai-sdk/openai-compatible",
"name": "Internal LLM Gateway",
"options": {
"baseURL": "https://llm-gateway.example.corp/v1",
"apiKey": "{env:INTERNAL_LLM_API_KEY}"
},
"models": {"coding-model": {"name": "Internal Coding Model", "tool_call": true, "limit": {"context": 128000, "output": 4096}}}
}
},
"model": "corp/coding-model",
"small_model": "corp/coding-model"
}
```
`limit`(여기서는 context 128000·output 4096)은 OpenCode 자신의 선제적 compaction과 올바른 `max_tokens`를 켜는 값이므로, 실제 gateway/모델의 한계에 맞춰 채웁니다. Linux managed config 경로는 `/etc/opencode/opencode.json`, macOS는 `/Library/Application Support/opencode` 또는 MDM profile `ai.opencode.managed`입니다. custom 파일은 `OPENCODE_CONFIG`, inline 설정은 `OPENCODE_CONFIG_CONTENT`를 쓸 수 있습니다. OpenCode의 기본 모델 대신 이 모델로 고정하고 결과에 모델 이름을 표시하려면 [3) Claude Code 등록](#3-claude-code-등록)에서 `--env OPENCODE_MCP_DEFAULT_MODEL=corp/coding-model`을 추가합니다(모델 없는 turn도 크기 추정과 동시 실행 cap의 대상이 되도록 하는 역할도 합니다).
### 2) opencode-mcp 빌드
저장소를 받은 뒤 저장소 루트에서 빌드합니다.
```sh
npm ci && npm run build && npm run bundle
```
단일 번들 `dist/opencode-mcp.mjs`가 생성됩니다(Node.js 20 이상). 예를 들어 `/opt/opencode-mcp/opencode-mcp.mjs`에 두고 실행할 수 있습니다. 조직 registry 게시본을 설치할 수도 있습니다(공개 unscoped `opencode-mcp`는 사용하지 않습니다).
```sh
npm install -g @internal/opencode-mcp
```
`@internal`은 placeholder이므로 사내 scope로 바꿉니다. 설치본의 실행 파일은 `opencode-mcp`입니다. 패키지 게시·미러링은 [배포 체크리스트 §1](docs/deployment-airgap.md#1-패키지와-아티팩트-미러링)을 참고하세요.
### 3) Claude Code 등록
`claude mcp add`에서는 `--env KEY=value` 옵션을 서버 실행 명령보다 먼저 두고, `--` 뒤에 실행 파일과 인자를 둡니다. 환경 변수가 여러 개면 `--env`를 반복합니다. 예시는 번들 실행 기준입니다.
```sh
claude mcp add --transport stdio opencode \
--env OPENCODE_MCP_OPENCODE_BIN=/usr/local/bin/opencode \
--env OPENCODE_MCP_ALLOWED_ROOTS=/workspace/project \
-- node /opt/opencode-mcp/opencode-mcp.mjs
```
프로젝트 `.mcp.json`으로 등록하는 예:
```json
{
"mcpServers": {
"opencode": {
"type": "stdio",
"command": "node",
"args": ["/opt/opencode-mcp/opencode-mcp.mjs"],
"env": {
"OPENCODE_MCP_OPENCODE_BIN": "/usr/local/bin/opencode",
"OPENCODE_MCP_ALLOWED_ROOTS": "/workspace/project"
}
}
}
}
```
조직 단위 배포는 [Claude Code 관리 등록 및 allowlist 절](#설치-claude-code-등록)을 참고하세요.
### 4) 동작 확인
`claude mcp list` 결과에 `opencode`가 있는지 확인하고, Claude Code에서 `/mcp`(Claude Code 문서 확인)로 연결 상태를 봅니다. 이어 Claude에게 “opencode-info로 서버 상태를 보여줘”라고 요청해 `connectionState`와 defaults를 확인합니다. 첫 `opencode-info` 호출 전에는 managed OpenCode 프로세스가 아직 시작되지 않습니다. 첫 호출의 `not_started` 확인은 [e2e 검증 기록](e2e/README.md#f9-v03-feature-scenarios-featurestestmjs-own-run-e2esh-step)에 있습니다.
사내 모델 연결은 Claude에게 “opencode-info로 모델 목록 보여줘”라고 요청해 확인합니다. `models` 결과에 사내 provider의 모델(예: `corp/coding-model`)이 있어야 하고, 외부 provider를 막았다면 다른 provider의 모델은 없어야 합니다. 저장소별 `opencode.json`을 쓴다면 “opencode-info로 <저장소 경로>의 모델 목록 보여줘”처럼 `cwd`를 지정해 확인합니다(그 경로는 허용 루트 안에 있어야 합니다). 이어 짧은 작업을 맡겨 답이 오는지 보고 `opencode-end`로 정리합니다. `OPENCODE_MCP_DEFAULT_MODEL`이나 호출의 `model`을 지정하지 않으면 결과에 사용 모델이 표시되지 않고 `opencode-info`의 `defaults.model`도 `null`입니다. 실제로 호출된 모델은 gateway 로그로 확인하세요. 모델이 없거나 인증 오류가 나면 [1) OpenCode 모델 설정](#1-opencode-모델-설정)의 확인 항목을 점검하세요.
### 5) 사용 예
Claude Code에 자연어로 요청할 수 있습니다.
- 단일 위임: “opencode에게 README 오탈자 수정을 맡기고 결과를 요약해줘.” 순서는 `opencode` → `opencode-reply` → `opencode-end`입니다.
- 검토 전용: “이 변경을 수정하지 말고 검토만 해줘.” 기본 샌드박스를 `read-only`로 설정합니다.
- 병렬 위임: “두 모듈을 각각 검토하고 모두 끝나면 비교해줘.” 각 `opencode` 호출에 `wait-seconds: 0`을 사용하고 `opencode-status`에 `ids`와 `wait-for: "all"`을 전달합니다.
- 긴 결과와 변경 확인: “완료되면 전체 답변과 변경 diff를 확인해줘.” `opencode-output`의 `answer`와 `diff` section을 사용합니다.
입력 예와 병렬 처리·결과 확인의 상세 레시피는 [서브에이전트 위임 레시피](#서브에이전트-위임-레시피)를 참고하세요.
### 6) 자주 쓰는 설정
- `OPENCODE_MCP_DEFAULT_SANDBOX`: 기본 `workspace-write`; 검토 전용이면 `read-only`를 권장합니다.
- `OPENCODE_MCP_ALLOWED_ROOTS`: 허용 작업 루트. 경로는 절대 경로이며 여러 경로는 플랫폼 경로 구분자로 나눕니다.
- `OPENCODE_MCP_DEFAULT_MODEL`: 기본 모델, `provider/model` 형식. 지정하지 않으면 모델을 보내지 않으므로 OpenCode 기본 모델(agent의 `model`, 없으면 설정의 `model`)을 씁니다.
- `OPENCODE_MCP_DEFAULT_APPROVAL_POLICY`: `never`(기본값) 또는 `on-request`.
- `OPENCODE_MCP_MAX_SESSIONS`: 추적 가능한 세션 수, 기본값 256, 최댓값 10000.
- `OPENCODE_MCP_SERVER_URL`: 기존 OpenCode 서버에 붙이는 attach 모드 URL. 지정하면 기본 모드도 `attach`가 됩니다.
- `OPENCODE_MCP_MAX_RUNNING_TURNS` / `_MAX_QUEUED_TURNS`: 동시 실행 run slot 수(기본 4)와 대기열 크기(기본 64). 초과분은 자동으로 큐에 들어갑니다.
- `OPENCODE_MCP_MODEL_PROFILES`: 모델별 context/output/input과 `maxRunning`을 알려주는 JSON. `OPENCODE_MCP_CONTEXT_GUARD`(기본 `reject`)로 이 budget을 벗어나는 prompt를 제출 전에 거부할지 정합니다.
run cap·대기열·모델 profile을 함께 지정하는 예 (`claude mcp add`):
```sh
claude mcp add --transport stdio opencode \
--env OPENCODE_MCP_MAX_RUNNING_TURNS=4 \
--env OPENCODE_MCP_MAX_QUEUED_TURNS=64 \
--env OPENCODE_MCP_MODEL_PROFILES='{"corp/coder-32k":{"context":32768,"output":4096,"maxRunning":2},"corp/big":{"maxRunning":1}}' \
--env OPENCODE_MCP_DEFAULT_MODEL=corp/coder-32k \
-- node /opt/opencode-mcp/opencode-mcp.mjs
```
같은 설정을 프로젝트 `.mcp.json`의 `env`로 적으면:
```json
{
"mcpServers": {
"opencode": {
"type": "stdio",
"command": "node",
"args": ["/opt/opencode-mcp/opencode-mcp.mjs"],
"env": {
"OPENCODE_MCP_MAX_RUNNING_TURNS": "4",
"OPENCODE_MCP_MAX_QUEUED_TURNS": "64",
"OPENCODE_MCP_MODEL_PROFILES": "{\"corp/coder-32k\":{\"context\":32768,\"output\":4096,\"maxRunning\":2},\"corp/big\":{\"maxRunning\":1}}",
"OPENCODE_MCP_DEFAULT_MODEL": "corp/coder-32k"
}
}
}
}
```
전체 값과 검증 규칙은 [환경 변수 표](#설정-환경-변수)를 참고하세요.
### 7) 제거
Claude Code에서 `claude mcp remove opencode`를 실행합니다(Claude Code 문서 확인). 프로젝트 `.mcp.json`으로 등록했다면 해당 `mcpServers.opencode` 항목도 제거하고, 직접 배치한 `/opt/opencode-mcp/opencode-mcp.mjs` 번들을 삭제합니다. 조직 등록 제거 및 세션 데이터 보존은 [배포 체크리스트 §9](docs/deployment-airgap.md#9-롤백-및-제거)에 따르세요.
## 빠른 시작
`opencode`로 시작하고 응답에서 `structuredContent.content`를 읽습니다. 같은 세션으로 `opencode-reply`를 보내고, 마지막에 `opencode-end`를 호출합니다. `wait-seconds`를 생략하면(Codex의 `codex`/`codex-reply`와 동일하게) turn이 끝날 때까지 블로킹하는 것이 기본 동작입니다.
1. `opencode`: `{"prompt":"README.md의 오탈자를 수정해 주세요.","cwd":"/workspace/project"}`
2. 응답 예시(축약, 종료 상태까지 블로킹한 뒤 반환됨):
```json
{"structuredContent":{"kind":"turn","sessionId":"ses_a81f…","threadId":"ses_a81f…","turnId":"ses_a81f…#1","turn":1,"status":"completed","executionState":"stopped","cleanup":"complete","content":"오탈자를 수정했습니다.","filesChanged":["README.md"],"toolCalls":[{"tool":"edit","status":"completed"}],"toolCallCount":1}}
```
3. `opencode-reply`: `{"sessionId":"ses_a81f…","prompt":"변경 내용을 설명해 주세요."}`
4. `opencode-end`: `{"sessionId":"ses_a81f…"}`
텍스트 응답도 `structuredContent.content`를 반영합니다.
### 서브에이전트 위임 레시피
- **fan-out/fan-in:** 각 작업은 `wait-seconds:0`으로 시작해 admission 직후 ID를 받습니다. `opencode-status`의 `ids`는 1–16개의 서로 다른 ID(각 1–200자)를 받으며 `wait-for:"any"`는 하나, `"all"`은 전체가 준비될 때까지 기다립니다. 끝난 ID는 다음 `ids`에서 제거하고 `detail:"compact"`으로 문맥을 아낍니다.
```json
{"prompt":"모듈 A 검토","cwd":"/workspace/project","wait-seconds":0}
{"prompt":"모듈 B 검토","cwd":"/workspace/project","wait-seconds":0}
{"ids":["ses_a81f…","ses_b72e…"],"wait-for":"any","wait-seconds":30,"detail":"compact"}
```
- **안전한 재시도:** 같은 논리 호출을 재시도할 때 같은 `request-id`를 사용합니다. 동일 입력은 prompt를 재전송하지 않고 원 작업에 합류하며, 입력이 다르면 `REQUEST_ID_CONFLICT`입니다. 최대 4096개, 종료 항목 24시간 보관의 프로세스 내 dedup이며 재시작 후 exactly-once를 보장하지 않습니다. 같은 request-id의 원 호출이 아직 admission되지 않은 상태에서 재시도가 도착하면, 그 재시도의 대기가 끝날 때 `REQUEST_PENDING`이 반환됩니다(같은 키로 다시 재시도하거나 `opencode-status`로 확인하세요).
```json
{"prompt":"테스트를 실행해 주세요","request-id":"run-tests-42","wait-seconds":0}
```
- **구조화 보고서:** `output-schema`는 현재 turn에만 적용됩니다. 아래 호출처럼 `summary`, `files[{path,status}]`, `testsPassed`를 요청하고 완료 후 `structuredOutputStatus`가 `valid`/`missing`/`invalid`인지 확인합니다. 유효한 구조도 신뢰할 수 없는 모델 출력입니다.
```json
{"prompt":"변경 사항을 JSON 보고서로 요약해 주세요.","output-schema":{"type":"object","properties":{"summary":{"type":"string"},"files":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"status":{"type":"string","enum":["added","modified","deleted"]}},"required":["path","status"],"additionalProperties":false}},"testsPassed":{"type":"boolean"}},"required":["summary","files","testsPassed"],"additionalProperties":false}}
```
- **답변 페이지와 diff:** `opencode-output`의 `section:"answer"`에서 이전 페이지의 `nextOffset`을 사용합니다. 변경은 `section:"diff", diff-view:"stat"`으로 찾은 뒤 `snapshot-id`와 `file-index`로 `patch`를 읽습니다. diff는 해당 turn의 사용자 메시지 기준 OpenCode snapshot이며 완전성이 보장되지 않습니다. 빈 결과는 무변경의 증거가 아닙니다.
```json
{"sessionId":"ses_a81f…","turn":1,"section":"answer","offset":0,"limit":4000}
{"sessionId":"ses_a81f…","turn":1,"section":"diff","diff-view":"stat"}
{"sessionId":"ses_a81f…","turn":1,"section":"diff","diff-view":"patch","file-index":0,"snapshot-id":"diff-…"}
```
- **같은 저장소 병렬 편집:** 호출자가 각 작업용 git worktree를 만들고 `cwd`로 전달합니다. 각 경로는 `OPENCODE_MCP_ALLOWED_ROOTS` 아래여야 하며 서버는 worktree를 관리하지 않습니다.
```sh
git worktree add ../wt-a -b task-a
```
- **모델/agent 찾기:** `opencode-info`의 `models`와 `agents` section이 광고하는 값을 선택해 `model`(`provider/model`)과 `agent`에 전달합니다.
```json
{"section":"models","cwd":"/workspace/project"}
{"section":"agents","cwd":"/workspace/project"}
```
### 모델별 컨텍스트에 맞춘 작업 배분
Claude Code는 작업을 보내기 전에 `opencode-info section:"models"`로 각 모델의 `limit`(`context`/`input`/`output`), `usableInputTokens`, `maxRunning`, `serverDefault`를 확인합니다.
- `usableInputTokens`는 OpenCode가 compaction 없이 쓸 수 있는 입력 budget이며, OpenCode 자신의 시스템 프롬프트·도구 설명(약 8천 토큰)이 이미 그 안에 포함되어 있습니다. 실제 prompt와 파일에 쓸 수 있는 여유는 이보다 작습니다.
- 크기가 중요한 작업은 `model`을 명시적으로 지정하세요. `opencode`(start)는 `model`을 생략하면 설정된 `OPENCODE_MCP_DEFAULT_MODEL`을 쓰고, `opencode-reply`는 생략 시 그 세션에 이미 저장된 모델을 그대로 이어받습니다 — prompt 크기 가드와 모델별 cap은 이렇게 결정되는 유효 모델이 전혀 없을 때만(기본 모델도, 세션에 저장된 모델도 없을 때) 건너뜁니다. `context`(`context.model` 포함)는 turn이 끝난 뒤가 아니라 usage 메타데이터가 포함된 assistant 메시지가 처음 관측되는 즉시 나타나므로, 아직 진행 중인 turn의 running snapshot에도 실릴 수 있습니다.
- prompt와 (agent가 읽을) 파일을 합쳐 budget보다 충분히 작게 유지하세요. 대략 ASCII 4자 ≈ 토큰 1개로 어림잡습니다(비-ASCII는 2자 ≈ 토큰 1개). 더 큰 작업은 여러 세션/모델로 나누세요 — 예를 들어 분석은 context가 큰 모델로, 모듈별 편집은 작은 모델로 각 세션/`cwd`에 나눠 맡깁니다.
- turn 결과의 `context{model,used,peakUsed,usableInputTokens,ratio,compacted,limitSource}`를 확인하세요. `ratio`가 0.8 이상이면 `CONTEXT_HIGH` 경고가 붙습니다 — 이때는 그 세션을 계속 채우지 말고 자기 완결적인(self-contained) prompt로 새 `opencode` 세션을 시작하고, 기존 세션은 `opencode-end`로 정리하세요.
- `peakUsed`는 이번 turn에서 context가 실제로 가장 커졌던 지점입니다(마지막 assistant 메시지 기준인 `used`와 달리, 중간에 compaction이 있었어도 가려지지 않습니다). 다음 분할 크기를 정할 때는 `used`보다 `peakUsed`를 기준으로 삼으세요.
`opencode-info` models 항목 예시:
```json
{"model":"corp/coder-32k","defaultForProvider":true,"limit":{"context":32768,"output":4096},"usableInputTokens":28672,"limitSource":"profile","maxRunning":2}
```
context 경고가 붙은 turn 결과 예시:
```json
{"status":"completed","context":{"model":"corp/coder-32k","used":24100,"peakUsed":24700,"usableInputTokens":28672,"ratio":0.841,"compacted":false,"limitSource":"profile"},"warnings":[{"code":"CONTEXT_HIGH","message":"Last reported context usage is 84% of corp/coder-32k's budget (24100 of 28672 tokens)."}],"hint":"… Context is nearly full; continue in a new opencode session with a self-contained prompt and opencode-end this one. If OpenCode itself has no context limit configured for this model, it will not compact proactively."}
```
### 병렬 실행과 최대 실행 개수
opencode-mcp 프로세스 하나는 기본적으로 `OPENCODE_MCP_MAX_RUNNING_TURNS`(기본 4)개의 turn만 동시에 실행합니다. 더 많은 작업을 보내면 자동으로 대기열(FIFO, 기본 최대 `OPENCODE_MCP_MAX_QUEUED_TURNS`=64)에 들어갔다가 자리가 나는 대로 시작되므로, 클라이언트 쪽에서 직접 동시 실행 수를 제한할 필요가 없습니다.
- 각 작업을 `wait-seconds:0`으로 시작하세요. cap을 넘는 초과분도 바로 큐에 들어가며 거부되지 않습니다(큐까지 가득 찬 경우는 예외, 아래 `RUN_QUEUE_CAPACITY` 참고).
- 결과에 `queue{position,running,maxRunning,blockedBy,model?,modelRunning?,modelMaxRunning?,queuedMs}` 객체가 있으면 아직 run slot을 기다리는 중입니다. `status`는 그래도 `"running"`이고 `resendSafety`는 `"not_submitted"`이지만, **다시 보내지 마세요** — 자리가 나면 자동으로 시작됩니다.
- 모든 작업은 `opencode-status`의 `ids`(한 번에 최대 16개)로 모읍니다. `wait-for:"all"`/`"any"`와 `wait-seconds`를 평소처럼 쓰면 됩니다.
- 아직 큐에 있는 turn은 `opencode-cancel`로 취소할 수 있습니다(업스트림에 아무것도 보내지 않았으므로 `not_submitted`로 끝납니다).
- `OPENCODE_MCP_MODEL_PROFILES`의 `maxRunning`으로 모델별 동시 실행 상한을 따로 둘 수 있습니다. 전역 cap에 여유가 있어도 특정 모델은 그 모델의 cap만큼만 동시에 돕니다.
- run slot과 큐가 모두 찼으면 새 `opencode`/`opencode-reply` 호출은 제출 전에 `RUN_QUEUE_CAPACITY`로 거부됩니다(아무것도 보내지 않습니다). 실행 중인 turn이 끝나길 기다렸다가 다시 시도하세요.
- `wait-seconds`는 큐 대기 시간도 포함해서 관찰 시간을 제한합니다 — 큐에 있어도 `wait-seconds:0`은 즉시 반환됩니다. 반대로 `timeout-seconds`로 재는 turn 실행 제한 시간은 제출 이후부터 측정되므로 큐 대기 시간은 포함하지 않습니다.
큐에 쌓인 항목 예시:
```json
{"status":"running","executionState":"active","resendSafety":"not_submitted","content":"","queue":{"position":3,"running":4,"maxRunning":4,"blockedBy":"global","queuedMs":1200},"hint":"This turn is queued for a run slot and will start automatically; keep observing it with opencode-status and do not resend it."}
```
`opencode-info section:"models"` 항목 예시(모델별 cap):
```json
{"model":"corp/big","limit":{"context":200000,"input":150000,"output":8192},"usableInputTokens":141808,"limitSource":"opencode","maxRunning":1}
```
### 비동기(폴링) 예시
즉시 반환받고 나중에 직접 상태를 확인하려면 `wait-seconds: 0`으로 시작한 뒤 `opencode-status`로 폴링합니다.
1. `opencode`: `{"prompt":"대규모 리팩터링을 진행해 주세요.","cwd":"/workspace/project","wait-seconds":0}` → 즉시 `status:"running"`과 `sessionId`를 반환합니다.
2. `opencode-status`: `{"sessionId":"ses_a81f…","wait-seconds":30}` → 최대 30초까지 관찰하고(`status` 최대값은 600), 완료 전이면 다시 호출합니다. 대기 중 발생한 승인 요청도 함께 전달됩니다.
3. `status`가 `completed`/`failed`/`cancelled`/`timeout`이 되면 `structuredContent.content`를 읽습니다.
## 도구 레퍼런스
입력은 strict schema이며 알 수 없는 속성은 거부됩니다. `sessionId`, `threadId`, `conversationId`는 별칭입니다. ID를 받는 도구는 정확히 하나를 요구하며 `opencode-status`만 ID 없이 목록을 조회할 수 있습니다.
| 도구 | 속성 | 타입 · 기본값 | 의미 |
|---|---|---|---|
| `opencode` | `prompt` | string · 필수 | 첫 요청 |
| | `cwd` | string · 서버 기본 경로 | 작업 디렉터리; `%` 문자가 있으면 `INVALID_ARGUMENT`로 거부됩니다 |
| | `model`, `agent` | string · 설정 기본값 | 모델(`provider/model`) 및 agent |
| | `sandbox` | enum · `workspace-write` | `read-only`, `workspace-write`, `danger-full-access` |
| | `approval-policy` | enum · `never` | `never` 또는 `on-request` |
| | `base-instructions`, `developer-instructions`, `title` | string · 미설정 | 지침 및 제목 |
| | `timeout-seconds` | 양의 정수 · 서버 설정 | turn 제한 시간(run slot을 기다리는 대기열 시간은 포함하지 않으며, 제출 이후부터 측정됩니다) |
| | `wait-seconds` | 0 이상 정수 · 미설정(=turn 종료까지 블로킹) | 반환 전 관찰 시간. 스키마 자체에는 상한이 없지만, 엔진이 `OPENCODE_MCP_MAX_TURN_TIMEOUT_SECONDS`(기본 21600)보다 큰 값을 거부하지 않고 그 값으로 **클램프**합니다. run slot이 없어 대기열에서 기다리는 시간도 이 관찰 시간에 포함되므로, `0`이면 대기 중이어도 즉시 `queue` 객체와 함께 반환됩니다 |
| | `request-id` | string · 선택 | 영숫자로 시작하는 1–128자 `[A-Za-z0-9._:-]`; opencode/reply 간 공유 |
| | `output-schema` | object · 선택 | turn-local JSON Schema subset; 잘못된 subset은 mutation 전에 `INVALID_OUTPUT_SCHEMA` |
| | `detail` | `standard` \| `compact` · `standard` | compact는 toolCalls/filesChanged를 생략하고 counts를 표시 |
| | `max-output-chars` | 정수 0–44000 · standard 44000 / compact 2000 | 서버 cap 이하의 이번 content 상한; 0은 빈 content |
| `opencode-reply` | `prompt` | string · 필수 | 후속 요청 |
| | ID 별칭 3종 | string · 정확히 하나 필수 | 이어갈 세션 |
| | `model`, `agent`, `developer-instructions` | string · 기존 값 유지 | 다음 turn 설정. sandbox와 approval-policy는 세션 고정 |
| | `timeout-seconds`, `wait-seconds` | 양의 정수 / 0 이상 · 미설정(=turn 종료까지 블로킹) | turn 제한 / 대기(마찬가지로 최대 turn timeout으로 클램프) |
| | `request-id`, `output-schema`, `detail`, `max-output-chars` | 위와 같음 | `opencode`와 같은 규칙; schema는 다음 turn에 상속되지 않음 |
| `opencode-status` | ID 별칭 3종 | string · 생략 시 목록 | 상태 확인 또는 추적 세션 목록 |
| | `wait-seconds` | 0–600 정수 · `0` | 상태 관찰(최대 600초); 대기 중 승인 요청을 전달할 수 있음. turn이 아직 admission(연결/warm-up 등) 단계이거나 run slot 대기열에 있어도 이 시간 안에 반환됩니다 |
| | `ids` | string[] · 1–16개, 각 1–200자, unique | batch 모드; ID 별칭과 상호 배타 |
| | `wait-for` | `any` \| `all` · `any` | ids 전용; 하나 또는 전체 항목이 준비되기를 기다림 |
| | `detail`, `max-output-chars` | 위와 같음 · batch는 compact | max-output-chars는 응답 전체의 aggregate answer budget, 기본 min(서버 cap, 8000) |
| `opencode-output` | ID 별칭, `turn` | ID 하나; turn 정수 ≥1 · 필수 | 보존된 turn 결과 읽기 |
| | `section` | `answer` \| `tool-calls` \| `structured-output` \| `diff` · `answer` | 읽을 artifact |
| | `offset` | 정수 ≥0 · `0` | 텍스트는 UTF-16 offset, 목록은 항목 offset; total과 같으면 빈 마지막 page, 초과면 `INVALID_ARGUMENT` |
| | `limit` | section별 · 기본 answer/structured-output/patch 4000자, tool-calls 20개, diff stat 50개 | text는 256–20000자; tool-calls/diff stat은 1–100개; 서버 cap 이하 |
| | `diff-view`, `file-index`, `snapshot-id` | `stat` \| `patch` · `stat`; 정수 ≥0; string 1–200자 | diff 전용; patch는 index와 snapshot 필수, stat offset>0 continuation은 snapshot 필수 |
| `opencode-info` | `section` | `server` \| `models` \| `agents` \| `roots` · `server` | 서버, advertised model/agent 또는 허용 루트 조회 |
| | `cwd`, `provider` | string · 선택 | cwd는 models/agents용, provider는 models용 |
| | `offset`, `limit` | offset 0–10000 · 0; limit 1–100 · 50 | models/agents/roots pagination |
| | `snapshot-id` | string 1–200자 · offset>0에서 필수 | pagination snapshot 계속 읽기; server section에는 pagination 불가 |
| `opencode-cancel` | ID 별칭 3종 | string · 정확히 하나 필수 | 실행 turn 취소, 세션 유지 |
| `opencode-end` | ID 별칭 3종 | string · 정확히 하나 필수 | turn 중지와 세션 정리 |
| | `action` | enum · `delete` | `delete` 또는 `archive` |
`output-schema`는 직렬화 16 KiB 이하, 깊이 12 이하, 노드 512개 이하이며 root는 `type:"object"`이어야 합니다. 허용 keyword는 `type`, `properties`, `required`, `additionalProperties`, `items`, `enum`, `description`, `maxLength`, `maxItems`뿐입니다. 타입은 object/array/string/number/integer/boolean/null(단일 타입 문자열)입니다. object는 `additionalProperties:false`, 최대 64개 property, 선언된 이름만 포함하는 중복 없는 `required`를 사용합니다. array는 단일 `items`; enum은 타입 호환 scalar 최대 64개; description 최대 1000자; maxLength 0–20000; maxItems 0–1000입니다. `$ref`, URL, union, pattern 및 알 수 없는 keyword는 허용되지 않습니다.
`opencode-end`는 이미 끝난 ID에도 멱등적으로 응답합니다(`not_found`). `opencode-cancel`은 turn만 취소하므로 세션으로 다시 reply할 수 있습니다. `opencode-end` 자체가 실패하면(삭제/보관 요청 실패 또는 정리 확인 실패) 세션은 `quarantined` 상태로 유지되며 `CLEANUP_UNCONFIRMED`가 반환됩니다 — 같은 `action`으로 `opencode-end`를 다시 호출해 재시도하세요(다른 action으로 바꾸면 다시 거부됩니다). `OPENCODE_MCP_ON_EXIT=end`로 종료할 때도 이런 세션을 재시도합니다.
## 결과 형식과 상태
`structuredContent.kind`는 `turn`, `sessions`, `end`, `error`, `output`, `info`, `batch` 중 하나입니다. 공통 필드는 `status`, `content`입니다.
- **turn**: `sessionId`, `threadId`, `turnId`, `turn`, `executionState`, `cleanup`, `directory`, `filesChanged`, `toolCalls`, `toolCallCount`, `pendingApprovals`, `elapsedMs`, `truncated`, `hint`; 가능하면 모델·agent·error·tokens(`input/output/reasoning`, 있으면 `cache{read,write}`)·cost 포함. run slot을 기다리는 동안은 `queue{position,running,maxRunning,blockedBy,model?,modelRunning?,modelMaxRunning?,queuedMs}`가 붙고, 대기열을 거쳤다면(또는 큐에서 취소·타임아웃됐다면) 승인 뒤에도 누적 대기 시간 `queuedMs`가 남습니다. usage가 관찰된 assistant가 있으면 `context{model,used,peakUsed,usableInputTokens,ratio,limitSource,compacted}`가 붙습니다([모델별 컨텍스트에 맞춘 작업 배분](#모델별-컨텍스트에-맞춘-작업-배분) 참고).
- **turn 확장 필드**: `output`은 보존 상태(`pending|retained|unavailable`), 사유(`expired|evicted|too_large`), `answerChars`, `toolCallCount`, `structuredChars`, `partial`, `expiresAt`를 담습니다. `structuredOutputStatus`는 `valid|missing|invalid`; 유효할 때만 완전한 `structuredOutput`, 오류면 `{code,message}`인 `structuredOutputError`가 올 수 있습니다. 추출 오류 코드는 `JSON_MISSING`, `JSON_AMBIGUOUS`, `JSON_PARSE_ERROR`, `OUTPUT_TOO_LARGE`, `SCHEMA_MISMATCH`입니다. `request` receipt는 `{id,serverInstanceId,replayed,scope:"process",expiresAt?}`입니다. 이 메타데이터와 출력 형식은 실행 완료 판정을 바꾸지 않습니다.
- **과부하·답변 판정 필드**: `finish`는 OpenCode의 종료 사유, `warnings`는 `EMPTY_RESPONSE`, `TRUNCATED`, `NONSTANDARD_FINISH`, `CONTEXT_HIGH`(ratio ≥ 0.8) 진단 목록입니다. `resendSafety`는 재전송 판단의 증거 수준입니다: `not_submitted`는 요청을 보내지 않았거나 미실행이 확인됨, `no_observed_effects`는 완전하고 정리된 기록에서 도구·patch 효과가 관찰되지 않음(“안전함”보다 약함), `inspect_effects`는 도구/patch 효과가 관찰됨, `unknown`은 기록·실행·정리가 불확실함을 뜻합니다. `upstreamRetry`는 `{attempt,message,nextAt?,observedAt}` provider 재시도 정보, `upstreamRead`는 `{state:"degraded",reason,statusCode?,since,nextAt}` 읽기 지연 정보, `responseLoop`는 `{count,windowMs,pattern}` 반복 응답 증거입니다. provider `error`에는 확인 가능한 `statusCode`, `retryable`, `retryAfterSeconds`와 `condition:"MODEL_OVERLOADED"`가 포함될 수 있습니다. batch turn 항목에도 같은 필드가 적용됩니다.
- 단일 turn의 `status`가 `failed` 또는 `timeout`이면 MCP 결과는 이제 `isError:true`입니다. `structuredContent`는 그대로 함께 제공됩니다. `completed`(경고 포함), `cancelled`, `running`은 `isError:true`가 아닙니다.
- **compact/축약**: compact turn은 `toolCalls`, `filesChanged`를 생략하고 `toolCallCount`, `filesChangedCount`, `pendingApprovalCount`를 유지합니다. `omittedFields`는 생략된 필드 이름을, `truncated`는 잘림을 표시합니다. batch compact 항목에도 `toolCallCount`, `filesChangedCount`, `pendingApprovalCount`가 있을 수 있습니다. 직렬화 객체는 45,000자 미만이어야 합니다.
- **output**: `opencode-output`는 `turnId`, `turn`, `section`, `offset`, `nextOffset`, `total`, `hasMore`, `partial`, `truncated` 및 해당 페이지의 `toolCalls`/`diff`를 반환합니다. diff metadata는 `source:"opencode-snapshot"`, `scope:"user-message"`, `sourceMessageId`, `snapshotId`, `observedAt`, `completeness:"not-guaranteed"`, `compacted`, `view`를 포함합니다. 텍스트 `content`는 페이지 텍스트이며 tool-call/diff-stat은 요약 content와 구조화 항목을 반환합니다.
- **info**: `opencode-info` 결과는 `section`, `truncated`와 선택적 `server`, `models`, `agents`, `roots`, pagination metadata를 반환합니다. server는 버전/모드/defaults(`contextGuard` 포함)/limits/concurrency/capabilities를 담습니다. `limits`에는 `maxSessions`, `maxRunningTurns`(`null`=무제한), `maxQueuedTurns`, `queueTimeoutSeconds`(`null`=비활성화)가 있고, `concurrency`에는 `{running,queued,heldUnknown,available,perModel,perModelTotal,perModelTruncated}`(`perModel`은 설정된 모델별 cap과 현재 실행/대기 수, 최대 32개)가 있습니다. `capabilities`에는 `run-queue`, `model-limits`, `context-usage`가 포함됩니다. models는 `model`, `providerId`, `modelId`, `defaultForProvider`(OpenCode 자신의 provider별 정렬 기본값이며 `OPENCODE_MCP_DEFAULT_MODEL`과는 무관), 선택적 `toolcall`, `limit`, `usableInputTokens`, `limitSource`, `maxRunning`, `serverDefault`(이 서버의 `OPENCODE_MCP_DEFAULT_MODEL`과 일치하는 모델)를 투영하며, agents는 `name`/`mode`를 투영합니다.
- **batch**: `{status:"ready"|"waiting",waitFor,reason:"condition"|"deadline",results,readyIds,pendingIds,truncated}`. `results`에는 입력 순서대로 각 ID의 turn snapshot(요청한 `model`과 `queue`/`queuedMs`/`context`가 있으면 함께 포함) 또는 `{sessionId,status:"error",error:{name,message}}`가 들어갑니다. 관찰 target은 호출 시작 시 캡처되며 알 수 없는 ID도 batch 전체 실패를 일으키지 않습니다.
- **sessions**: 세션 항목은 `sessionId`, `title`, `directory`, `status`, `turns`, `updatedAt`, run slot을 기다리는 turn이 있으면 `queue`; 목록 결과에 `opencodeVersion`이 있을 수 있습니다.
- **end**: `status`는 `ended` 또는 `not_found`; `action`, `abortedRunningTurn`, `cleanup` 포함.
- **error**: `status: "failed"`, `error: {name,message}`; 세션 ID가 있을 수 있습니다.
Turn 상태는 `running`, `waiting_for_approval`, `completed`, `failed`, `cancelled`, `timeout`입니다. 세션 목록에는 `idle`, `ending`, `quarantined`도 있습니다(`quarantined`: 실행 결과가 아직 확인되지 않았거나 `opencode-end`가 실패해 재시도가 필요한 상태). `executionState`는 `active`/`stopped`/`unknown`, `cleanup`은 `complete`/`unconfirmed`입니다. `filesChanged`는 OpenCode patch에서 얻은 최선의 추정이며 빈 배열은 무변경의 증명이 아닙니다.
| 오류 코드 | 대응 |
|---|---|
| `INVALID_ARGUMENT` | 입력과 ID 별칭을 확인합니다. |
| `PATH_NOT_ALLOWED` | 허용 루트 아래의 `cwd`를 선택하거나 관리자가 허용 루트를 조정합니다. |
| `SESSION_NOT_FOUND` | 새 `opencode` 호출로 시작합니다. v0.1은 재시작 후 세션을 다시 채택하지 않습니다. |
| `SESSION_BUSY` | 상태를 확인하거나 대기하고 필요하면 취소합니다. |
| `OPENCODE_UNAVAILABLE` | 실행 파일, 서버 URL·상태, 네트워크를 점검합니다. |
| `UPSTREAM_ERROR` | OpenCode 응답과 내부 gateway 로그를 확인합니다. |
| `OPENCODE_OVERLOADED` | admission/read에서 HTTP `429`/`503`/`529`, 또는 세션 생성/prompt에서 HTTP `429`를 받았습니다. prompt는 제출되지 않았습니다. `retryAfterSeconds`까지 기다린 뒤 한 번 재시도합니다. |
| `SUBMISSION_UNCONFIRMED` | 재전송하지 말고 `opencode-status`로 기존 세션을 관찰합니다. |
| `CLEANUP_UNCONFIRMED` | 취소/정리가 확인되지 않았습니다. 세션은 `quarantined`로 표시됩니다; 같은 action으로 `opencode-end`를 재시도하세요. |
| `TURN_INCOMPLETE` | 최종 응답 없이 idle이 됐습니다. 상태를 확인하고 새 지시로 재시도합니다. |
| `SHUTTING_DOWN`, `INTERNAL` | 재연결하거나 서버 로그를 확인합니다. |
| `TURN_NOT_FOUND` | turn 번호가 기록되지 않았거나 세션이 끝났습니다. `opencode-status`로 현재 turn을 확인합니다. |
| `OUTPUT_NOT_READY` | turn이 아직 실행 중입니다. `opencode-status`로 기다린 뒤 다시 읽습니다. |
| `OUTPUT_UNAVAILABLE` | 출력이 만료·퇴거·초과 크기로 사용할 수 없습니다. 복구되지 않습니다. |
| `OUTPUT_LIMIT_TOO_SMALL` | 서버 출력 cap이 section의 최소 256자보다 작습니다. 관리자가 `OPENCODE_MCP_MAX_OUTPUT_CHARS`를 올려야 합니다. |
| `SNAPSHOT_EXPIRED` | offset 0, snapshot-id 없이 새 snapshot을 요청합니다. |
| `UPSTREAM_RESPONSE_TOO_LARGE` | OpenCode 응답이 허용 크기를 넘었습니다. 범위를 좁히거나 나중에 재시도합니다. |
| `EMPTY_RESPONSE` | 텍스트 없이 끝났고 도구/patch 효과도 관찰되지 않았습니다. gateway 응답을 확인한 뒤 재시도 여부를 판단합니다. |
| `UPSTREAM_RESPONSE_LOOP` | 반복되는 비정상 provider 응답으로 turn이 중지됐습니다. `responseLoop`와 부분 효과를 확인합니다. |
| `ContentFilterError` | provider가 답변을 필터링했습니다. 입력 정책과 gateway 응답을 확인합니다. |
| `INVALID_OUTPUT_SCHEMA` | 지원 subset에 맞게 `output-schema`를 수정합니다. |
| `REQUEST_ID_CONFLICT` | 다른 입력에 이미 사용된 키입니다. 새 request-id를 사용하거나 생략합니다. |
| `REQUEST_UNCONFIRMED` | 도달 여부가 불명확합니다. 재시도하지 말고 `opencode-status`로 먼저 확인합니다. |
| `REQUEST_ENDED` | 해당 키의 turn이 종료됐습니다. 새 요청에는 새 키를 사용합니다. |
| `REQUEST_CAPACITY` | request-id 테이블이 찼습니다. 키 없이 재시도하거나 만료를 기다립니다. |
| `REQUEST_PENDING` | 같은 request-id의 원 호출이 아직 admission 중입니다. 같은 키로 잠시 후 재시도하거나 `opencode-status`로 확인합니다. |
| `SESSION_CAPACITY` | 추적 중인 세션이 너무 많습니다. `opencode-end`로 끝난 세션을 정리하거나 관리자가 `OPENCODE_MCP_MAX_SESSIONS`를 올려야 합니다. |
| `RUN_QUEUE_CAPACITY` | run slot과 대기열이 모두 찼습니다. 제출 전 거부이며 아무것도 보내지 않았습니다(`opencode-reply`라면 기존 세션은 그대로입니다). 실행 중인 turn이 끝나길 기다렸다가 재시도하세요(`opencode-info section:"server"`의 `concurrency` 확인). |
| `PROMPT_TOO_LARGE` | prompt만으로 선택한 모델의 입력 budget을 heuristic으로 초과한다고 판단했습니다. 대부분 제출 전 거부(세션 없음)입니다. 모델의 budget을 warm-up 이후에야 알 수 있었던 드문 경우에는 idle 세션이 남은 `failed` turn(`error.name:"PROMPT_TOO_LARGE"`, `retryable:false`)으로 보고됩니다 — 그때는 `opencode-reply`로 더 작은 prompt나 다른 `model`을 보내거나 `opencode-end`로 정리하세요. 어느 경우든 작업을 나누거나 더 큰 context 모델을 지정하세요(`opencode-info section:"models"`). |
다른 모델 오류는 turn의 `error.name`으로 반환될 수 있습니다. 그중 다음 둘은 이 기능이 추가하는 이름입니다.
| turn `error.name` | 대응 |
|---|---|
| `QUEUE_TIMEOUT` | `OPENCODE_MCP_QUEUE_TIMEOUT_SECONDS` 동안 run slot을 못 받았습니다. `failed`, `resendSafety:"not_submitted"`, `retryable:true`이며 세션은 idle로 남습니다 — 새 세션을 시작하지 말고 같은 세션에 `opencode-reply`로 다시 보내세요. |
| `ContextOverflowError` | prompt 또는 세션 기록이 모델의 context window를 넘어 OpenCode가 reject했고 재시도(reactive compaction)로도 회복하지 못했습니다. 작업을 나누거나 새 세션을 시작하거나 더 큰 context 모델을 선택하세요(`opencode-info section:"models"`). 이 이벤트가 나와도 마지막 assistant가 성공했다면 turn은 그대로 `completed`로 보고됩니다(재시도 compaction으로 복구된 경우). |
`error.retryable`은 일시적 장애라는 뜻이지 prompt를 다시 보내도 된다는 허가가 아니다 — `executionState`, `cleanup`, `resendSafety`를 먼저 확인합니다.
기본 `OPENCODE_MCP_MAX_OUTPUT_CHARS=20000`은 content 제한입니다. 모든 가변 길이 필드(제목/디렉터리/상태 문자열, 패턴, 배열 크기 등)를 먼저 개별적으로 제한한 뒤에도 직렬화 결과가 45,000자 이상이면 toolCalls 제거, filesChanged 축소, sessions 축소, pendingApprovals 축소, content 축소 순서로 추가 축약해 항상 45,000자 미만을 보장합니다. 오류 메시지 2,000자, 짧은 문자열 필드(파일 경로·패턴·세션 title/directory/status 등) 300자, filesChanged 200개, toolCalls 20개, 승인 10개(승인당 패턴 20개), 세션 100개가 개별 상한이며 축약 여부는 `truncated`로 나타납니다.
## 설치: Claude Code 등록
### 내부 npm registry
공개 unscoped npm 이름 `opencode-mcp`는 다른 게시자 소유입니다. 프록시에서 이 패키지를 설치하지 마세요. `@internal/opencode-mcp`의 `@internal`은 placeholder이며 사내 scope와 registry로 바꿉니다.
`package.json`은 실수로 공개 registry에 게시되는 것을 막기 위해 기본적으로 `"private": true`입니다. 사내 게시 파이프라인은 실제 scope로 이름을 바꿀 때 이 값도 함께 `false`로 바꿔야 합니다(그렇지 않으면 `npm publish`가 거부합니다). 게시 대상은 다음 중 하나로 지정합니다.
- `package.json`에 `"publishConfig": {"registry": "https://npm.internal.example/"}`를 추가한 사내 포크를 유지하거나,
- 공유 설정을 건드리지 않고 `npm publish --registry https://npm.internal.example/`을 그때그때 지정합니다.
`npm pack`/`npm publish` 전에는 `prepack` 스크립트가 `npm run build && npm run bundle`을 실행해 `dist/`(gitignore 대상)를 채웁니다.
```sh
npm install -g @internal/opencode-mcp
claude mcp add --transport stdio opencode --env OPENCODE_MCP_OPENCODE_BIN=/usr/local/bin/opencode -- opencode-mcp
```
`claude mcp add` 인자 순서: `--env KEY=value`는 서버 명령 앞에 두고 `--` 뒤에 실행 파일과 인자를 둡니다. 환경 변수가 여럿이면 `--env`를 반복합니다.
### 단일 파일 번들
`npm run bundle` 산출물 `dist/opencode-mcp.mjs`를 `/opt/opencode-mcp/` 같은 경로에 복사합니다. Node.js 20 이상이 필요합니다. 번들은 MCP/Zod 의존성만 포함하고 OpenCode 실행 파일은 별도입니다.
아래 형식은 프로젝트 `.mcp.json` 예시입니다.
```json
{
"mcpServers": {
"opencode": {
"type": "stdio",
"command": "node",
"args": ["/opt/opencode-mcp/opencode-mcp.mjs"],
"env": {"OPENCODE_MCP_OPENCODE_BIN": "/usr/local/bin/opencode"}
}
}
}
```
조직 관리 설정은 Linux에서 `/etc/claude-code/managed-mcp.json`에 둘 수 있습니다(같은 `mcpServers` 형식). 정책 allowlist의 argv는 `command`와 `args`를 합친 정확한 배열이어야 합니다.
```json
{
"allowManagedMcpServersOnly": true,
"allowedMcpServers": [
{"serverCommand": ["node", "/opt/opencode-mcp/opencode-mcp.mjs"]}
]
}
```
Claude Code의 기본 tool 호출 hard limit은 100,000,000ms(약 27.8시간)이며, 서버별 `timeout`은 이 값을 대체하므로 더 낮게 설정하면 호출이 먼저 취소될 수 있습니다. 지정한다면 최대 turn 시간에 시작·정리 여유를 더한 값(기본 최대 기준 약 21,700,000ms 이상)으로 설정하세요. 30분 idle timeout은 진행 heartbeat로 처리하므로 이를 이유로 더 짧은 hard limit을 지정할 필요는 없습니다. `CLAUDE_CODE_MCP_ALLOWLIST_ENV=1`은 환경 제한 스위치입니다. 설정하면 `HOME`, `PATH`, `CLAUDE*`와 서버 `env`에 적은 값만 MCP 서버에 전달되므로 proxy(`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`), CA(`NODE_EXTRA_CA_CERTS`), attach 및 gateway 변수를 서버 `env`에 넣습니다. 비밀값을 저장소에 기록하지 마세요. (Claude Code 동작은 [검증 조사](docs/research/mcp-client.md) §3.9 참고.)
이 서버 `env`와는 별도로, opencode-mcp가 자신이 띄우는 `opencode serve` 자식에게 전달할 환경도 `OPENCODE_MCP_CHILD_ENV_ALLOWLIST`로 제한하길 권장합니다 — 아래 [보안 모델과 한계](#보안-모델과-한계)를 참고하세요.
## 설정: 환경 변수
빈 문자열은 미설정으로 처리됩니다. boolean은 대소문자 무관 `1/0`, `true/false`, `yes/no`; 정수/초 값은 양의 정수여야 하며, 초 값은 ms 환산 시 2,147,483,647(2^31-1)을 넘지 않도록 2,147,483초를 초과할 수 없습니다(Node `setTimeout` overflow로 더 큰 값은 타임아웃을 사실상 즉시 발동시키기 때문). enum은 열거된 값만 받으며 설정 오류는 변수명을 표시합니다.
| 환경 변수 | 기본값 | 의미와 검증 |
|---|---|---|
| `OPENCODE_MCP_MODE` | URL 설정 시 `attach`, 아니면 `managed` | `managed` 또는 `attach` |
| `OPENCODE_MCP_SERVER_URL` | 없음 | attach 필수; 유효 http(s), userinfo 금지, 비-loopback은 HTTPS 기본 |
| `OPENCODE_MCP_ALLOW_INSECURE_HTTP` | `false` | loopback 외 HTTP 허용 |
| `OPENCODE_SERVER_USERNAME` | `opencode` | OpenCode server 사용자명 |
| `OPENCODE_SERVER_PASSWORD` | 없음 | attach 암호; managed 자식에는 임의 암호 생성 |
| `OPENCODE_MCP_OPENCODE_BIN` | `opencode` | 실행 파일 경로/이름 |
| `OPENCODE_MCP_SERVE_ARGS` | 빈 값 | 공백 구분 인자; `--hostname`, `--port`, `--mdns*`, `--cors` 금지 |
| `OPENCODE_MCP_AIRGAP` | `true` | managed 자식에 air-gap 기본값 적용 |
| `OPENCODE_MCP_CHILD_ENV_ALLOWLIST` | 빈 값 | 쉼표 구분 이름/`PREFIX_*`; 비면 환경 상속 후 scrub |
| `OPENCODE_MCP_DEFAULT_CWD` | `CLAUDE_PROJECT_DIR`, 아니면 프로세스 cwd | 절대 경로 |
| `CLAUDE_PROJECT_DIR` | 미설정 | 기본 cwd fallback, 지정 시 절대 경로 |
| `OPENCODE_MCP_ALLOWED_ROOTS` | 기본 cwd | `path.delimiter` 구분 허용 루트; 각 항목은 trim되며 절대 경로여야 함; 명시적으로 설정했는데 유효 항목이 0개면 시작 실패 |
| `OPENCODE_MCP_REMOTE_PATHS` | `false` | 원격 경로의 local realpath 검사 생략 |
| `OPENCODE_MCP_DEFAULT_MODEL` | 없음(OpenCode 기본 모델 사용) | 기본 `provider/model`; 설정 시 반드시 `provider/model` 형식이어야 함(아니면 시작 실패) |
| `OPENCODE_MCP_DEFAULT_AGENT` | 없음 | 기본 agent |
| `OPENCODE_MCP_DEFAULT_SANDBOX` | `workspace-write` | `read-only`, `workspace-write`, `danger-full-access` |
| `OPENCODE_MCP_DEFAULT_APPROVAL_POLICY` | `never` | `never`, `on-request` |
| `OPENCODE_MCP_TURN_TIMEOUT_SECONDS` | `3600` | 기본 turn 제한 초 |
| `OPENCODE_MCP_MAX_TURN_TIMEOUT_SECONDS` | `21600` | 최대 turn 제한 초; 기본값 이상 |
| `OPENCODE_MCP_APPROVAL_TIMEOUT_SECONDS` | `600` | 승인 대기 제한 초 |
| `OPENCODE_MCP_STARTUP_TIMEOUT_SECONDS` | `60` | 시작 제한 초. Node fetch/undici의 headers/body timeout으로 실제 HTTP 대기는 최대 300초 |
| `OPENCODE_MCP_REQUEST_TIMEOUT_SECONDS` | `30` | HTTP 요청 제한 초. Node fetch/undici의 headers/body timeout으로 실제 HTTP 대기는 최대 300초 |
| `OPENCODE_MCP_CLEANUP_TIMEOUT_SECONDS` | `15` | 취소/정리 제한 초 |
| `OPENCODE_MCP_HEARTBEAT_SECONDS` | `15` | 진행 heartbeat 간격 초; 최대 `600`(Claude Code의 30분 idle 중단을 이 heartbeat로 막으므로 더 큰 값은 거부됩니다) |
| `OPENCODE_MCP_STATUS_POLL_SECONDS` | `30` | 상태 polling 간격 초 |
| `OPENCODE_MCP_SSE_STALL_SECONDS` | `35` | SSE 무응답 제한 초 |
| `OPENCODE_MCP_READ_RETRY_ATTEMPTS` | `3` | admission GET 시도 횟수(최초 포함), 1–3 정수; 오류: `must be an integer between 1 and 3, got "<값>"` |
| `OPENCODE_MCP_RESPONSE_LOOP_LIMIT` | `6` | 10초 안의 연속 응답 루프 임계값, 3–20 정수; `0`은 비활성화. 오류: `must be 0 (disabled) or an integer between 3 and 20, got "<값>"` |
| `OPENCODE_MCP_MAX_OUTPUT_CHARS` | `20000` | 양의 정수 content 제한 |
| `OPENCODE_MCP_MAX_SESSIONS` | `256` | 동시 추적 세션(활성+quarantined) 상한, 양의 정수 ≤ 10000; 초과 시 새 `opencode` 시작은 어떤 upstream 변경도 없이 `SESSION_CAPACITY`로 거부되며 기존 세션은 절대 evict되지 않음 |
| `OPENCODE_MCP_MAX_RUNNING_TURNS` | `4` | 동시 실행 가능한 turn(run slot) 수, `0`은 무제한; 정수 0–256. 오류: `must be an integer between 0 and 256, got "<값>"` |
| `OPENCODE_MCP_MAX_QUEUED_TURNS` | `64` | 대기열에 쌓일 수 있는 turn 수, 정수 0–1024(`0`은 대기열 없음 — 큐가 가득 차면 즉시 `RUN_QUEUE_CAPACITY`). 오류: `must be an integer between 0 and 1024, got "<값>"` |
| `OPENCODE_MCP_QUEUE_TIMEOUT_SECONDS` | `0`(비활성화) | 대기열에서 run slot을 기다리는 최대 초; `0` 또는 1–2147483 정수(대기열은 upstream에 아무것도 붙잡지 않으므로 기본은 무제한 대기). 오류: `must be a positive integer (seconds), got "<값>"`, 2147483초 초과 시 `must not exceed 2147483 seconds` |
| `OPENCODE_MCP_MODEL_PROFILES` | `{}` | 모델별 `{context?, input?, output?, maxRunning?}` JSON 객체, 키는 `provider/model`(예: `{"corp/coder-32k":{"context":32768,"output":4096,"maxRunning":2},"corp/big":{"maxRunning":1}}`; `maxRunning`만 있는 profile도 유효). 최대 256개 키. 검증 오류: JSON이 아니면 `must be valid JSON`; 객체가 아니면 `must be a JSON object`; 256개 초과면 `must contain at most 256 model profiles`; 키 형식 오류면 `model key "<model>" must use provider/model with valid 1..200 character parts`; 값이 객체가 아니면 `profile for "<model>" must be an object`; 알 수 없는 필드면 `profile for "<model>" has unknown field "<field>"`; 필드 범위 오류면 `profile field "<field>" for "<model>" must be an integer between 1 and <field 최대값>`(`context`/`input`/`output`은 100000000, `maxRunning`은 256); `maxRunning`이 `OPENCODE_MCP_MAX_RUNNING_TURNS`보다 크면 `profile maxRunning for "<model>" must not exceed OPENCODE_MCP_MAX_RUNNING_TURNS`; `context`/`output` 중 하나만 있으면 `profile for "<model>" must specify context and output together`; `input`이 `context`보다 크면 `profile input for "<model>" must not exceed context` |
| `OPENCODE_MCP_CONTEXT_GUARD` | `reject` | `reject` 또는 `off`; `reject`면 prompt가 알려진 budget을 heuristic(ASCII 4자/non-ASCII 2자당 토큰 1개, 세션 기록 미포함)으로 명백히 초과할 때 제출 전에 `PROMPT_TOO_LARGE`로 거부합니다. 오류: `must be one of reject\|off, got "<값>"` |
| `OPENCODE_MCP_END_ACTION` | `delete` | `delete` 또는 `archive` |
| `OPENCODE_MCP_ON_EXIT` | `abort` | `abort` 또는 `end` |
| `OPENCODE_MCP_LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` |
## opencode-mcp가 하는 일과 하지 않는 일
### 네트워크
- opencode-mcp는 OpenCode 서버에만 HTTP로 접속합니다. managed 모드에서는 직접 시작한 `http://127.0.0.1:<임의 포트>`, attach 모드에서는 `OPENCODE_MCP_SERVER_URL`을 사용합니다. 텔레메트리, 업데이트 확인, npm 호출, 원격 스키마 다운로드는 없습니다.
- 요청은 리다이렉트를 따라가지 않으며(`redirect: 'error'`), 고정 경로에 기본 URL을 붙여 구성하므로 다른 호스트로 새지 않습니다.
- attach URL은 http 또는 https만 허용하고 userinfo를 금지합니다. 루프백이 아닌 주소는 기본적으로 https여야 합니다. 평문 http는 `OPENCODE_MCP_ALLOW_INSECURE_HTTP=1`로 명시 허용할 수 있습니다.
- managed OpenCode는 `127.0.0.1`에만 바인딩되고 mDNS가 꺼져 있으며, 실행마다 32바이트 임의 비밀번호를 사용합니다. `OPENCODE_MCP_SERVE_ARGS`로 `--hostname`, `--port`, `--mdns`, `--cors`를 바꿀 수 없습니다.
- 기본 에어갭 설정(`OPENCODE_MCP_AIRGAP=1`)은 managed OpenCode의 자동 업데이트, 공유, LSP 다운로드, 모델 목록 조회를 끄고 npm 재시도를 0으로 둡니다. 이미 설정된 환경 변수는 덮어쓰지 않으며 네트워크를 차단하는 장치는 아닙니다.
### 파일과 세션
- opencode-mcp 자체는 파일을 만들거나 수정·삭제하거나 이름·권한을 바꾸지 않습니다. 파일 시스템 접근은 경로 확인용 `realpath`와 `stat`뿐이며, 로그는 stderr로만 출력하고 로그 파일을 만들지 않습니다.
- opencode-mcp는 OpenCode의 revert, shell, vcs, worktree, 설정 변경 API를 호출하지 않습니다. 호출자 프롬프트에 파일 수정 지시를 덧붙이지 않으며, 구조화 출력을 요청했을 때 JSON 형식 안내만 덧붙입니다.
- 중단·삭제·아카이브·권한 거부는 opencode-mcp가 직접 만든 세션에만 적용됩니다. 권한 요청에는 거부 또는 사람이 승인한 1회 허용으로만 답하고 항상 허용으로 답하지 않습니다.
- `opencode-end`를 action 없이 호출하면 기본 동작은 `delete`입니다. 해당 OpenCode 세션의 메시지와 기록을 영구 삭제하지만 작업 파일은 지우지 않습니다. 기본 종료 동작(`OPENCODE_MCP_ON_EXIT=abort`)은 실행 중인 턴만 중단하고 세션은 남겨 둡니다.
- managed 모드에서만 OpenCode의 `All fibers interrupted` 오류가 나면 해당 디렉터리의 OpenCode 인스턴스를 재시작(dispose)합니다. 파일과 세션 기록은 지워지지 않으며 attach 공유 서버에는 적용하지 않습니다.
### 프로세스
- opencode-mcp가 실행하는 프로그램은 고정된 작은 POSIX sh 감시 스크립트를 거치는 `opencode serve`와 프로세스 그룹 확인용 `ps`뿐입니다. 사용자 입력을 셸 문자열로 조합하지 않고 인자는 위치 인자로만 전달합니다. 종료할 때 자신이 띄운 OpenCode 프로세스 그룹에만 SIGTERM을 보내며, 5초 뒤에도 남아 있으면 SIGKILL을 보냅니다. attach 모드에서는 프로세스를 종료하지 않습니다.
- opencode-mcp가 SIGKILL·크래시로 강제 종료되어도 감시 스크립트가 부모 종료를 5초 간격으로 확인해 OpenCode에 SIGTERM을 보내고, 3초 뒤에도 남아 있으면 SIGKILL을 보냅니다. 따라서 약 5–8초 안에 `opencode serve`가 정리됩니다(부모 PID가 그 사이 다른 프로세스에 재사용되는 극히 드문 경우는 예외). 확인 명령: `ps -A -o pid,pgid,command | grep "opencode serve"`.
- 정상 종료(기본 `OPENCODE_MCP_ON_EXIT=abort`, managed 모드)에서는 종료 시작과 동시에 OpenCode 프로세스 그룹을 종료합니다. 실행 중인 턴은 프로세스 종료를 정지 증거로 삼아 `stopped`로 기록되며 완료로 보고되지 않습니다.
### 자원
- managed OpenCode는 OpenCode가 필요한 첫 호출(예: `opencode`) 때 시작하고(`opencode-info`의 server 조회는 시작하지 않음), 시작 후 opencode-mcp가 끝날 때까지 유지됩니다(idle 자동 종료 없음). 실행 중인 작업이 없으면 폴링, SSE, 타이머가 돌지 않습니다.
- 재연결 백오프(0.5–5초), 응답 크기(JSON 8MiB, 이벤트 1MiB, `/provider` catalog 32MiB 등), 메모리 캐시(출력 32MiB/1시간, 추적 세션 기본 256개)에 상한이 있습니다. `/provider` catalog가 32 MiB를 넘으면(`enabled_providers` 미설정 등) `UPSTREAM_RESPONSE_TOO_LARGE`입니다. turn은 기본 1시간, 최대 6시간의 timeout 뒤 자동 중단됩니다.
### OpenCode 자체가 하는 일
- OpenCode는 opencode-mcp와 별개로 LLM 제공자와 통신합니다. provider를 설정하지 않으면 OpenCode 기본 설정에 따라 외부 클라우드 provider(OpenCode Zen 등)를 쓸 수 있습니다. 첫 부팅 때 npm registry에서 `~/.config/opencode`에 plugin을 설치할 수 있고, `rg`가 없으면 ripgrep을 내려받을 수 있습니다. `~/.local/share/opencode`에는 세션 DB와 snapshot을 저장합니다.
- 기본 `workspace-write` sandbox에서는 요청에 따라 OpenCode가 bash, 파일 편집, webfetch/websearch를 쓸 수 있습니다. 파일 삭제나 `curl` 실행도 가능할 수 있습니다. sandbox는 OS 격리가 아니라 OpenCode 권한 프로필입니다. 검토 전용이면 `OPENCODE_MCP_DEFAULT_SANDBOX=read-only`를 쓰세요. 이 모드는 edit, bash, webfetch, websearch, external_directory를 차단합니다.
- managed OpenCode는 기본적으로 부모 환경 변수를 상속하지만 `ANTHROPIC_*`, `CLAUDE_*`, `OPENCODE_MCP_*`, `OPENCODE_SERVER_*` 등은 제거됩니다(감시 스크립트용 `OPENCODE_MCP_WATCHDOG_PPID`=opencode-mcp PID만 추가됨). 외부 통신 변수를 제한하려면 `OPENCODE_MCP_CHILD_ENV_ALLOWLIST`로 전달할 이름을 제한하세요.
## 과부하·비정상 응답 대응
opencode-mcp는 prompt를 자동 재전송하지 않습니다. admission GET 오류는 `OPENCODE_MCP_READ_RETRY_ATTEMPTS` 횟수(최초 포함) 안에서 startup deadline까지 제한 재시도합니다. 서버의 `Retry-After`는 최대 3600초까지 하한으로 존중하며, admission/cleanup deadline이 더 짧으면 기다렸다가 조기 재시도하지 않고 operation을 종료합니다. 세션 생성/prompt의 HTTP 429는 prompt 제출 전 `OPENCODE_OVERLOADED`로 반환됩니다.
실행 중 turn은 OpenCode 읽기가 느리거나 5xx여도 backoff 후 계속 진행하며 `upstreamRead`와 `OpenCode reads are delayed …` 진행 메시지를 표시할 수 있습니다. attach 모드는 health probe 세 번이 hard-fail된 뒤 서버 unreachable로 선언합니다. managed child는 느린/5xx 읽기 때문에 재시작하지 않습니다. OpenCode 1.18.33은 provider 자체에서 첫 시도 후 최대 5회(명목상 2/4/8/16/30초, `Retry-After` 존중) 재시도한 뒤 `APIError`를 낼 수 있으며 이 과정은 `upstreamRetry`로 표시됩니다.
| 답변 분류 | 결과 |
|---|---|
| `stop` + 빈/공백 응답, 효과 없음 | `failed`, `EMPTY_RESPONSE`, `retryable:true` |
| 텍스트 없이 tool/patch 활동 | `completed` + `EMPTY_RESPONSE` warning |
| `length` 종료 | `completed` + `TRUNCATED`, `output.partial:true`; 답변이 비면 `failed` + `EMPTY_RESPONSE` |
| `content-filter` 종료 | `failed`, `ContentFilterError` |
| 비표준 finish | `failed`, `TURN_INCOMPLETE` + `NONSTANDARD_FINISH` |
| 잘못된 streaming 응답 | `failed`, `UnknownError`와 고정된 정제 메시지 `The model provider returned a malformed streaming response.` |
OpenCode 1.18.33은 빈 stream, HTML 응답 또는 잘못된 tool JSON에서 초당 약 7–8회 재요청을 무한 반복할 수 있습니다. watchdog은 이 관찰된 서명만 인식하며, 10초 안에 `OPENCODE_MCP_RESPONSE_LOOP_LIMIT`회 연속 unusable response가 나오면 `failed`/`UPSTREAM_RESPONSE_LOOP`로 turn을 중지합니다. 실제 OpenCode 1.18.33 e2e에서 세 경우 모두 첫 모델 요청 후 약 1.5–4.5초, 모델 요청 9–30회 안에 중지됐습니다(최종 전체 실행: 각각 약 2.5초, 14–15회). 텍스트/추론이 스트리밍되는 동안(최근 10초 안에 delta 수신)은 watchdog 검사를 하지 않습니다.
재전송 판단: `not_submitted`는 제출되지 않았으므로 다시 보낼 수 있습니다. `no_observed_effects`는 “안전함” 보장이 아니므로 기록을 확인합니다. `inspect_effects`는 도구/patch 효과를 점검한 뒤 이어서 진행합니다. `unknown`이면 재전송하지 말고 `opencode-status`로 기존 세션을 관찰합니다.
## 보안 모델과 한계
v0.3 기능 한계: 보존 출력은 메모리에서 1시간, 최대 128 turns/32 MiB(각 turn 최대 4 MiB)이며 성공한 `opencode-end`와 프로세스 종료 시 사라집니다. `request-id`는 프로세스 내, 최대 4096개 기록의 24시간 dedup이며 crash-safe exactly-once가 아닙니다. diff는 해당 turn의 사용자 메시지 기준 OpenCode snapshot이라 완전성이 보장되지 않고, 빈 diff도 변경 없음의 증거가 아닙니다. `opencode-info`는 allowlist로 투영한 정보만 반환하고 provider key/options를 노출하지 않습니다. OpenCode `opencode-ai@1.18.33`에서는 prompt `format`이 message 읽기를 영구히 깨뜨리므로 사용하지 않습니다. 세션 수는 `OPENCODE_MCP_MAX_SESSIONS`(기본 256)로 제한되며 초과 시 `opencode-end`로 정리해야 새 세션을 시작할 수 있습니다.
run slot cap과 대기열 한계: `OPENCODE_MCP_MAX_RUNNING_TURNS`/`_MAX_QUEUED_TURNS`는 **이 opencode-mcp 프로세스** 단위입니다 — attach 모드로 여러 opencode-mcp 프로세스가 같은 OpenCode 서버에 붙으면 서버에 실제로 걸리는 동시 실행 수는 각 프로세스 cap의 합만큼 더 커질 수 있습니다. cap은 opencode-mcp가 추적하는 **turn 수**를 세는 것이지 상류(gateway) 요청 수가 아닙니다 — compaction이나 연속 단계처럼 한 turn 안에서 여러 번 LLM 요청이 발생할 수 있습니다. 승인 대기 중인 turn(`waiting_for_approval`)은 `OPENCODE_MCP_APPROVAL_TIMEOUT_SECONDS`(기본 600초)까지 run slot을 계속 점유합니다. 실행 결과가 불확실한(`executionState:"unknown"`) turn도 복구가 확인될 때까지 slot을 유지하며(`opencode-info section:"server"`의 `concurrency.heldUnknown`), 세션 복구·managed 프로세스 재시작·세션 종료 시 해제됩니다. 큐 대기 때문에 turn 시작이 늦어질 수 있으므로, Claude Code 쪽 MCP tool-call `timeout`을 짧게 지정했다면 이 대기 시간까지 포함해 여유를 두거나 `wait-seconds`로 직접 관찰하세요.
`OPENCODE_MCP_CONTEXT_GUARD`의 prompt 크기 추정은 ASCII 문자 4자당 토큰 1개, 비-ASCII 문자 2자당 토큰 1개로 어림잡는 휴리스틱이며 세션 기록은 포함하지 않습니다 — 과소·과대 추정이 있을 수 있고 실제 tokenizer가 아닙니다. `usableInputTokens`/`context.used`/`context.peakUsed` 등 사용량 수치는 OpenCode 1.18.33의 내부 계산을 그대로 반영한 것이며, 다른 OpenCode 버전에서는 달라질 수 있습니다.
**지연된 abort 한계**: abort 응답이 유실되거나 잘못된 형식이면 모호성 표식을 유지하고 두 번째 abort를 보내지 않습니다. 세션은 최대 `max(2 × request timeout, 60초)` 동안 격리됩니다. 그 뒤 idle/terminal 증거를 확인해 해제하며, 늦게 도착한 abort는 다음 turn을 중단시킬 수 있습니다. abort 이전의 도구 효과는 되돌리지 않으며, 해당 turn은 정상 완료로 보고되지 않습니다. `abort`/`delete`/`archive` 응답은 확정적인 4xx에서만 모호성 표식을 지우며 `408`/`429`는 제외됩니다.
응답 루프 watchdog은 관찰된 빈 응답, HTML 본문, 잘못된 tool JSON 패턴만 감지하며 `OPENCODE_MCP_RESPONSE_LOOP_LIMIT=0`이면 비활성화됩니다. 느린/실패한 OpenCode 읽기는 결과를 지연할 수 있고, 서버가 요청한 `Retry-After` 대기는 최대 3600초까지 하한으로 존중됩니다. 이미 관찰한 메시지가 OpenCode 기록에서 사라지면(예: 외부에서 revert) 읽기를 불일치로 보고 판정을 보류하므로 세션이 `quarantined`로 남을 수 있습니다. 이때는 `opencode-end`로 정리합니다.
`sandbox`는 OpenCode permission profile이지 OS 격리가 아닙니다. 모든 profile에서 `task`, `question`, `plan_enter`, `plan_exit`를 deny합니다. `read-only`는 `edit`, `bash`, `external_directory`, `webfetch`, `websearch`도 deny하고 `workspace-write`는 `external_directory`를 deny합니다. 규칙은 deny-only이며 `always` 승인은 하지 않습니다. `approval-policy=never`는 승인 요청을 거절합니다. `on-request`는 legacy elicitation만 사용하며 headless `claude -p` 또는 legacy UI가 없는 클라이언트에서는 요청을 거절합니다. elicitation 기능 자체를 전혀 선언하지 않는 연결은 승인 대기 없이 즉시 거절되고, 선언하는 연결은 SDK 기본값 60초가 아니라 남은 `OPENCODE_MCP_APPROVAL_TIMEOUT_SECONDS` 시간만큼 대기합니다.
managed child 환경은 `ANTHROPIC_*`, `CLAUDE_*`, `CLAUDECODE`, `AI_AGENT`, `OPENCODE_MCP_*`, `OPENCODE_SERVER_*`를 scrub합니다. `OPENCODE_MCP_CHILD_ENV_ALLOWLIST`가 비면 나머지 환경을 상속한 뒤 scrub하고, 설정하면 필수 `PATH`, `HOME`, `USER`, `LOGNAME`, `LANG`, `LC_ALL`, `TMPDIR`, `SHELL`, `TERM`과 허용 이름/접두사만 선택한 뒤 scrub합니다. attach URL은 loopback 외 HTTPS가 기본이며 `OPENCODE_MCP_ALLOW_INSECURE_HTTP=1`로 예외를 허용합니다. 허용 루트는 cwd 선택만 제한하고 전체 파일 접근을 가두지 않습니다. 강한 격리는 OS 사용자, 컨테이너/VM, 파일·네트워크 정책으로 구성하세요.
최소 권한 배포에서는 `OPENCODE_MCP_CHILD_ENV_ALLOWLIST`를 명시적으로 설정하길 권장합니다. 예: `OPENCODE_MCP_CHILD_ENV_ALLOWLIST=INTERNAL_LLM_API_KEY,NPM_CONFIG_*,NODE_EXTRA_CA_CERTS,HTTP_PROXY,HTTPS_PROXY,NO_PROXY,OPENCODE_*` (OpenCode가 실제로 필요로 하는 이름에 맞게 조정; 자세한 설명은 [docs/deployment-airgap.md](docs/deployment-airgap.md) §5 참고).
**한계를 명시**: `OPENCODE_SERVER_PASSWORD`는 managed 자식의 환경 변수로 전달됩니다. 허용 루트 아래 checkout의 `.opencode/` plugin과 프로젝트 `opencode.json`은 sandbox 설정과 무관하게 shared `opencode serve` 안에서 실행될 수 있으며, server 환경의 비밀번호에도 접근할 수 있습니다. 또한 `sandbox`가 `workspace-write` 또는 `danger-full-access`이면 OpenCode 자신의 `bash` 도구가 이 환경을 읽거나(`env`, `printenv`) 그 비밀번호로 loopback API(`127.0.0.1:<port>`)를 호출할 수 있습니다(`read-only`는 `bash`를 deny). 프로젝트 설정/로컬 plugin을 쓰지 않을 운영자는 `OPENCODE_MCP_SERVE_ARGS=--pure` 또는 OpenCode 설정 `OPENCODE_DISABLE_PROJECT_CONFIG=1`을 선택할 수 있습니다. `--pure`는 외부 plugin을 건너뛰며 로컬 plugin을 로드하지 않는 것으로 검증됐고, `OPENCODE_DISABLE_PROJECT_CONFIG`는 프로젝트 설정을 무시하는 용도입니다([OpenCode 오프라인 조사](docs/research/opencode-offline.md) §6 "Switches and where each was verified"). `--pure`가 기본 적용되지는 않습니다. 프로젝트 설정을 허용하는 경우 OpenCode 프로세스 자체를 신뢰 경계 안에 두어야 합니다.
입력 schema 위반은 MCP SDK 단계에서 거부되어 `isError`와 `Input validation error: …` 텍스트로 반환되며 `structuredContent`가 없습니다. `INVALID_ARGUMENT`는 handler의 의미 검사(예: ID 별칭 개수) 오류에만 해당합니다.
## 문제 해결
- **긴 작업/Claude Code timeout:** 약 15초 간격 heartbeat를 보내므로 30분 idle timeout을 처리합니다. 대화형 Claude Code는 약 120초 뒤 호출을 background로 옮길 수 있고 `TaskStop`으로 중단할 수 있습니다. per-server hard `timeout`을 지정한다면 최대 turn 시간과 시작·정리 여유보다 길게 둡니다. heartbeat는 전체 wall-clock 제한을 늘리지 않습니다.
- **세션 격리 또는 busy:** 상태/정리가 확인되지 않으면 새 turn이 차단될 수 있습니다. `opencode-status`, OpenCode 프로세스와 로그를 확인합니다.
- **권한 거절 뒤 `TURN_INCOMPLETE`:** terminal assistant 답 없이 idle일 수 있습니다. 결과를 확인하고 `opencode-reply`로 새 지시를 보냅니다.
- **ripgrep 누락:** OpenCode 프로세스의 `PATH`에 `rg`를 설치하고 server를 재시작합니다(실패 경로가 캐시될 수 있음).
- **느린 첫 시작:** OpenCode가 config-dir에 `@opencode-ai/plugin` 및 dependency를 npm 설치할 수 있습니다. 내부 registry와 CA를 제공하거나 사전 준비하세요.
- **`EMPTY_RESPONSE`:** provider가 텍스트 없이 끝났습니다. warning과 `toolCalls`/`filesChanged`를 확인합니다.
- **`TRUNCATED` warning:** provider가 `length`로 끝냈습니다. `output.partial`을 확인하고 작업을 나누거나 provider 출력 제한을 점검합니다.
- **`UPSTREAM_RESPONSE_LOOP`:** `responseLoop.pattern`을 보고 빈 stream, HTML 또는 tool JSON 형식을 조사합니다. gateway 복구 후 같은 세션에 새 지시를 보낼 수 있습니다.
- **`OPENCODE_OVERLOADED`:** `retryAfterSeconds`까지 기다린 뒤 한 번 재시도합니다. prompt는 제출되지 않았습니다.
- **`OpenCode reads are delayed` 진행 메시지:** OpenCode 상태 읽기가 느리거나 실패 중입니다. `upstreamRead.nextAt` 이후 상태를 확인합니다.
- **`SUBMISSION_UNCONFIRMED`:** 재전송하지 말고 `opencode-status`로 기존 세션을 관찰합니다.
- **오프라인 모델 목록:** 내장 catalog snapshot이 표시될 수 있습니다. 내부 gateway provider/model을 직접 구성합니다.
- **MCP 재시작 후 `Session not found`:** v0.1은 세션 adopt가 없으므로 `opencode`로 새 세션을 시작합니다.
- **보존 출력 만료/누락:** 1시간/용량 제한으로 artifact가 없으면 `OUTPUT_UNAVAILABLE`이 반환됩니다. 다시 얻으려면 새 turn이 필요합니다.
- **페이지 이어 읽기 실패:** `SNAPSHOT_EXPIRED`면 offset 0에서 snapshot-id를 빼고 다시 시작합니다. patch 조회는 `stat`에서 얻은 snapshot-id와 file-index를 사용합니다.
- **request-id 재사용 오류:** `REQUEST_ID_CONFLICT`는 같은 키에 다른 인자를 보낸 경우입니다. 새 논리 호출에는 새 키를 사용합니다. `REQUEST_UNCONFIRMED`에서는 먼저 상태를 확인하고 prompt를 재전송하지 않습니다.
- **구조화 출력 누락/오류:** `structuredOutputStatus`와 `structuredOutputError.code`를 읽습니다. 이 결과는 turn 실패나 재시도를 뜻하지 않습니다.
## 개발
Node runtime은 20 이상, 테스트는 Node 22.18 이상(type stripping)입니다.
| 명령 | 기능 |
|---|---|
| `npm run typecheck` | TypeScript 검사 |
| `npm run build` | 컴파일 |
| `npm run bundle` | `dist/opencode-mcp.mjs` 생성 |
| `npm test` | 테스트 |
| `npm run check` | typecheck 후 test |
`scripts/remote.sh <label> [--no-install] [--pull <path>] <command...>`는 원격 Docker 호스트의 `node:22`에서 실행합니다(기본은 npm install). 원격 호스트 기본값은 관리자 환경의 SSH 별칭 `gram`이며 `OCMCP_REMOTE_HOST`로 바꿀 수 있습니다(`e2e/run-e2e.sh`도 같음). 주요 경로: `src/config.ts`, `src/mcp/`, `src/core/`, `src/opencode/`, `test/`, `e2e/`, `docs/`.
### End-to-end 검증
`e2e/`는 실제 OpenCode 바이너리와 가짜(fake) LLM 서버를 붙여 stdio MCP 왕복 전체를 검증하는 hermetic 하네스입니다(네트워크 접근 없이 컨테이너 안에서 실행). 진입점은 `e2e/run-e2e.sh`이며 사용법과 시나리오 목록은 `e2e/README.md`를 참고하세요. 단위/통합 테스트(`npm test`)와 별개로, 실제 Claude Code 클라이언트/유사 SDK 클라이언트가 관찰하는 JSON-RPC 왕복까지 재현합니다.
## 라이선스와 버전
버전 **v0.4.0** (design v0.4 + 모델별 context 크기·run-slot 상한), 라이선스 MIT([LICENSE](LICENSE); OpenCode에서 캡처한 조사 자료의 고지는 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)). 조사에서 검증한 조합은 OpenCode `opencode-ai@1.18.33`, `@modelcontextprotocol/server@2.2.0`, Claude Code `2.1.284`, Node.js 20 이상(테스트 Node.js 22.18 이상), TypeScript `7.0.2`, `zod@4.6.5`, `esbuild@0.28.2`입니다.
TDQS
Scored across 7 tools
Each tool maps to a distinct stage of the session lifecycle (start, reply, status, cancel, output, info, end), so most are easy to tell apart. The only friction points are opencode-cancel vs opencode-end (both stop a running turn, though only end deletes the session) and opencode-status vs opencode-output (both surface turn content, though output pages the full retained text).
All tools share a clear `opencode-` prefix in consistent kebab-case, which reads cleanly as a family. The only deviation is the bare `opencode` create tool and the fact that suffixes mix nouns (status, output, info) with verbs (reply, cancel, end), but the pattern is still predictable.
Seven tools is well-scoped for a coding-agent delegation server, with each tool earning its place across session creation, continuation, monitoring, cancellation, output retrieval, discovery, and teardown. Nothing seems redundant or filler.
The surface covers a full session lifecycle: create, reply, status/list, cancel, output paging, config/catalog discovery, and end/cleanup. Coverage is strong; only minor conveniences (e.g. a dedicated session-list beyond the status-without-id path) are absent, which agents can work around.