claude-opencode-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +514 -0
- package/SECURITY.md +80 -0
- package/agents/deepseek-coder.md +32 -0
- package/agents/deepseek-researcher.md +36 -0
- package/agents/deepseek-reviewer.md +31 -0
- package/agents/deepseek-tester.md +31 -0
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +208 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/config/config.d.ts +109 -0
- package/dist/config/config.js +163 -0
- package/dist/config/config.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +68 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/execute.d.ts +30 -0
- package/dist/mcp/execute.js +128 -0
- package/dist/mcp/execute.js.map +1 -0
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +38 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session-utils.d.ts +5 -0
- package/dist/mcp/session-utils.js +55 -0
- package/dist/mcp/session-utils.js.map +1 -0
- package/dist/mcp/tool-utils.d.ts +11 -0
- package/dist/mcp/tool-utils.js +44 -0
- package/dist/mcp/tool-utils.js.map +1 -0
- package/dist/mcp/tools/abort-session.d.ts +3 -0
- package/dist/mcp/tools/abort-session.js +48 -0
- package/dist/mcp/tools/abort-session.js.map +1 -0
- package/dist/mcp/tools/create-session.d.ts +3 -0
- package/dist/mcp/tools/create-session.js +53 -0
- package/dist/mcp/tools/create-session.js.map +1 -0
- package/dist/mcp/tools/delegate-task.d.ts +4 -0
- package/dist/mcp/tools/delegate-task.js +98 -0
- package/dist/mcp/tools/delegate-task.js.map +1 -0
- package/dist/mcp/tools/get-diff.d.ts +3 -0
- package/dist/mcp/tools/get-diff.js +97 -0
- package/dist/mcp/tools/get-diff.js.map +1 -0
- package/dist/mcp/tools/get-session.d.ts +3 -0
- package/dist/mcp/tools/get-session.js +79 -0
- package/dist/mcp/tools/get-session.js.map +1 -0
- package/dist/mcp/tools/list-agents.d.ts +3 -0
- package/dist/mcp/tools/list-agents.js +72 -0
- package/dist/mcp/tools/list-agents.js.map +1 -0
- package/dist/mcp/tools/send-message.d.ts +3 -0
- package/dist/mcp/tools/send-message.js +59 -0
- package/dist/mcp/tools/send-message.js.map +1 -0
- package/dist/opencode/agents.d.ts +46 -0
- package/dist/opencode/agents.js +239 -0
- package/dist/opencode/agents.js.map +1 -0
- package/dist/opencode/binary.d.ts +19 -0
- package/dist/opencode/binary.js +129 -0
- package/dist/opencode/binary.js.map +1 -0
- package/dist/opencode/client.d.ts +32 -0
- package/dist/opencode/client.js +55 -0
- package/dist/opencode/client.js.map +1 -0
- package/dist/opencode/manager.d.ts +66 -0
- package/dist/opencode/manager.js +492 -0
- package/dist/opencode/manager.js.map +1 -0
- package/dist/opencode/permission-watcher.d.ts +26 -0
- package/dist/opencode/permission-watcher.js +144 -0
- package/dist/opencode/permission-watcher.js.map +1 -0
- package/dist/opencode/run.d.ts +39 -0
- package/dist/opencode/run.js +318 -0
- package/dist/opencode/run.js.map +1 -0
- package/dist/opencode/sessions.d.ts +26 -0
- package/dist/opencode/sessions.js +105 -0
- package/dist/opencode/sessions.js.map +1 -0
- package/dist/opencode/setup.d.ts +38 -0
- package/dist/opencode/setup.js +109 -0
- package/dist/opencode/setup.js.map +1 -0
- package/dist/security/paths.d.ts +20 -0
- package/dist/security/paths.js +93 -0
- package/dist/security/paths.js.map +1 -0
- package/dist/security/policy.d.ts +17 -0
- package/dist/security/policy.js +86 -0
- package/dist/security/policy.js.map +1 -0
- package/dist/types/index.d.ts +131 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/util/fs.d.ts +12 -0
- package/dist/util/fs.js +73 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/git.d.ts +54 -0
- package/dist/util/git.js +202 -0
- package/dist/util/git.js.map +1 -0
- package/dist/util/logger.d.ts +21 -0
- package/dist/util/logger.js +129 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/util/text.d.ts +11 -0
- package/dist/util/text.js +126 -0
- package/dist/util/text.js.map +1 -0
- package/dist/workspace/context.d.ts +12 -0
- package/dist/workspace/context.js +67 -0
- package/dist/workspace/context.js.map +1 -0
- package/dist/workspace/resolver.d.ts +35 -0
- package/dist/workspace/resolver.js +112 -0
- package/dist/workspace/resolver.js.map +1 -0
- package/dist/workspace/validator.d.ts +13 -0
- package/dist/workspace/validator.js +102 -0
- package/dist/workspace/validator.js.map +1 -0
- package/docs/agent-configuration.md +100 -0
- package/docs/architecture.md +115 -0
- package/docs/claude-code-setup.md +116 -0
- package/docs/configuration.md +184 -0
- package/docs/contributing.md +71 -0
- package/docs/deepseek-setup.md +84 -0
- package/docs/development.md +146 -0
- package/docs/installation.md +88 -0
- package/docs/opencode-setup.md +97 -0
- package/docs/security-audit.md +80 -0
- package/docs/security.md +128 -0
- package/docs/troubleshooting.md +126 -0
- package/docs/workspace-model.md +126 -0
- package/package.json +79 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Security audit
|
|
2
|
+
|
|
3
|
+
- **Version audited:** 1.0.0
|
|
4
|
+
- **Date:** 2026-09-24
|
|
5
|
+
- **Scope:** the `claude-opencode-mcp` source tree, its configuration parsing,
|
|
6
|
+
process management, permission model, MCP tool surface, CI workflows, and
|
|
7
|
+
published package contents.
|
|
8
|
+
|
|
9
|
+
## Method
|
|
10
|
+
|
|
11
|
+
1. Manual review of every source module with attention to command execution,
|
|
12
|
+
path handling, credential flow, and permission construction.
|
|
13
|
+
2. Pattern scan for unsafe primitives: `child_process.exec`, `execSync`,
|
|
14
|
+
`shell: true`, `eval`, `new Function`, non-local `http://` endpoints.
|
|
15
|
+
3. Secret scan of the repository (API-key and private-key patterns) and a scan
|
|
16
|
+
for machine-specific paths in shipped files.
|
|
17
|
+
4. Dependency audit: `npm audit` and `npm audit --omit=dev`.
|
|
18
|
+
5. Review of the permission model against the documented threat model,
|
|
19
|
+
including how a model could bypass intended restrictions.
|
|
20
|
+
6. Review of CI workflows for secret exposure and fork safety.
|
|
21
|
+
7. Reproduction of security-relevant behavior with the automated test suite,
|
|
22
|
+
including real OpenCode/DeepSeek end-to-end tests.
|
|
23
|
+
|
|
24
|
+
## Findings
|
|
25
|
+
|
|
26
|
+
| # | Finding | Risk | Status |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| 1 | `opencode.binary` configuration was ignored; the bridge used a PATH-resolved binary instead. | A user explicitly selecting a trusted binary could unknowingly run another. | Fixed: resolver receives the configured path; regression test asserts the configured binary is used. |
|
|
29
|
+
| 2 | Shutdown/startup race could orphan a spawned OpenCode process. | Local resource leak; a stale server keeps running after the bridge exits. | Fixed: children are tracked at spawn time, startup aborts during shutdown, and a synchronous `exit`-handler kills anything left. Tested with a fake binary. |
|
|
30
|
+
| 3 | Session registry and log file were created with default permissions. | Other local users could read agent output summaries and workspace paths. | Fixed: state directory `0700`, files `0600`; log directory created with `0700`. |
|
|
31
|
+
| 4 | Logger had no redaction. | A future code path logging credentials would leak them to disk. | Fixed: log field names matching password/secret/token/authorization/API-key/credential/private-key are redacted; unit-tested. |
|
|
32
|
+
| 5 | MCP tool inputs were unbounded. | Oversized payloads could bloat prompts/logs. | Fixed: `.max()` limits on task, message, cwd, model, agent, title, session id, path array (200 items), and timeout (1 hour). |
|
|
33
|
+
| 6 | CI E2E job could attempt to use secrets on fork pull requests. | Failed/unsafe runs on untrusted PRs. | Fixed: the E2E job runs only on `push` events, and checkouts disable credential persistence. |
|
|
34
|
+
| 7 | `edit: deny` alone is bypassable by shell commands. | Read-only intent could be violated. | Mitigated: read-only profiles also deny `bash`; shell-enabled tester is documented as able to write via shell. Verified end-to-end that a read-only agent cannot create a file. |
|
|
35
|
+
|
|
36
|
+
No unsafe command construction was found: all external processes use
|
|
37
|
+
`execFile`/`spawn` with explicit argument arrays and no shell. Credentials for
|
|
38
|
+
external OpenCode servers come only from explicit configuration or the URL,
|
|
39
|
+
never from the ambient environment. Dependency audit reports zero known
|
|
40
|
+
vulnerabilities.
|
|
41
|
+
|
|
42
|
+
## Accepted risks
|
|
43
|
+
|
|
44
|
+
These are intentional and documented in [security.md](security.md):
|
|
45
|
+
|
|
46
|
+
- Shell-enabled agents (`deepseek-coder`, `deepseek-tester`) can write files
|
|
47
|
+
through shell commands; the tester's edit tools are denied but shell is
|
|
48
|
+
inherently powerful.
|
|
49
|
+
- Sensitive-file protection applies to OpenCode's file tools, not to `bash`.
|
|
50
|
+
- A delegated agent reads repository content, so prompt injection from a
|
|
51
|
+
hostile repository is possible; review diffs with `get_diff` and keep
|
|
52
|
+
`workspace.allowedRoots` narrow.
|
|
53
|
+
- Explicitly configured/`OPENCODE_BIN` executables are trusted by definition.
|
|
54
|
+
- Changing `opencode.hostname` from `127.0.0.1` exposes the OpenCode HTTP API
|
|
55
|
+
to the network; use a server password and firewall the port.
|
|
56
|
+
|
|
57
|
+
## Security test coverage
|
|
58
|
+
|
|
59
|
+
The suite includes assertions for: workspace canonicalization and traversal
|
|
60
|
+
rejection, allowed-root enforcement, path-hint escape rejection, read-only
|
|
61
|
+
agent file-creation denial, `.env` secret non-disclosure, `git commit` denial,
|
|
62
|
+
unknown-session handling, timeout and abort without hanging, crashed-server
|
|
63
|
+
handling, workspace routing mismatch detection, child-process cleanup, log
|
|
64
|
+
redaction, and dependency freshness via CI.
|
|
65
|
+
|
|
66
|
+
## Reproducing
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm audit --omit=dev
|
|
70
|
+
npm audit
|
|
71
|
+
grep -rEn "exec\(|execSync|shell: true|eval\(|new Function\(" src/
|
|
72
|
+
grep -rEn "sk-[a-zA-Z0-9]{20,}|BEGIN (RSA|OPENSSH|EC) PRIVATE" .
|
|
73
|
+
npm test
|
|
74
|
+
npm run test:e2e # real OpenCode + provider required
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Reporting
|
|
78
|
+
|
|
79
|
+
Found something this audit missed? Please use the private process in
|
|
80
|
+
[SECURITY.md](../SECURITY.md).
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
Security in this bridge comes from four places: workspace validation in the
|
|
4
|
+
bridge, permission rules enforced by OpenCode, the absence of interactive
|
|
5
|
+
`ask` rules (headless safety), and honest documentation of the remaining
|
|
6
|
+
limits.
|
|
7
|
+
|
|
8
|
+
## Workspace boundaries
|
|
9
|
+
|
|
10
|
+
- The workspace must exist, be a directory, and be readable.
|
|
11
|
+
- Paths are canonicalized (`realpath`); `..` and symlinks cannot escape.
|
|
12
|
+
- `workspace.allowedRoots` (when non-empty) restricts every workspace to those
|
|
13
|
+
canonical roots.
|
|
14
|
+
- `paths` hints cannot escape the workspace (`INVALID_PATH`).
|
|
15
|
+
- `workspace.defaultCwd` is only a fallback; explicit `cwd` and
|
|
16
|
+
`CLAUDE_PROJECT_DIR` fail loudly when invalid.
|
|
17
|
+
- The bridge probes the OpenCode server's `directory` before the first run and
|
|
18
|
+
aborts on mismatch (`WORKSPACE_MISMATCH`).
|
|
19
|
+
|
|
20
|
+
## File protection
|
|
21
|
+
|
|
22
|
+
When `security.protectEnvFiles` is `true` (default), the read and edit
|
|
23
|
+
permission rules deny:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
*.env, *.env.*, *.pem, *.key, *.p12, *.pfx,
|
|
27
|
+
id_rsa*, id_ed25519*, id_ecdsa*, credentials, credentials.*, .ssh/*, *.npmrc
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
while keeping `*.env.example`, `*.env.sample` and `*.env.template` readable.
|
|
31
|
+
Add your own patterns with `security.extraProtectedPatterns`.
|
|
32
|
+
|
|
33
|
+
## Git safety
|
|
34
|
+
|
|
35
|
+
For agents that can run shell commands (`coder`, `tester`), bash rules deny:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
git push* git -C * push* git -c * push*
|
|
39
|
+
git commit* git -C * commit* git -c * commit*
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
All other commands are allowed for those profiles. Read-only profiles have
|
|
43
|
+
`bash: deny` outright, so they cannot mutate anything through the shell.
|
|
44
|
+
|
|
45
|
+
The bridge never commits, pushes, stages or reverts anything itself. There is
|
|
46
|
+
no automatic commit or push anywhere in the codebase.
|
|
47
|
+
|
|
48
|
+
## Permission matrix
|
|
49
|
+
|
|
50
|
+
| Permission | read | review | code | test |
|
|
51
|
+
| --- | --- | --- | --- | --- |
|
|
52
|
+
| `read` | allow (+ sensitive denies) | same | same | same |
|
|
53
|
+
| `edit` | deny | deny | allow (+ sensitive denies) | deny |
|
|
54
|
+
| `bash` | deny | deny | allow (git commit/push denied) | allow (git commit/push denied) |
|
|
55
|
+
| `glob`, `grep`, `list`, `lsp` | allow | allow | allow | allow |
|
|
56
|
+
| `webfetch`, `websearch`, `skill`, `todowrite` | allow | allow | allow | allow |
|
|
57
|
+
| `task` (subagents) | deny | deny | deny | deny |
|
|
58
|
+
| `external_directory` | deny | deny | deny | deny |
|
|
59
|
+
| `question`, `doom_loop` | deny | deny | deny | deny |
|
|
60
|
+
|
|
61
|
+
`externalDirectory` can be set to `allow`, `ask` or `deny`. `ask` is
|
|
62
|
+
discouraged: nothing answers the prompt in headless mode, although the bridge
|
|
63
|
+
auto-rejects permission events it sees.
|
|
64
|
+
|
|
65
|
+
## Headless safety
|
|
66
|
+
|
|
67
|
+
- No `ask` rules are emitted by the bridge.
|
|
68
|
+
- The bridge subscribes to OpenCode's event stream and rejects any permission
|
|
69
|
+
request for an active delegated session, logging a warning. A run cannot hang
|
|
70
|
+
waiting for a human.
|
|
71
|
+
- Every run has a timeout; on timeout the bridge aborts the OpenCode session
|
|
72
|
+
and returns `AGENT_TIMEOUT`.
|
|
73
|
+
- Cancellation via MCP (`extra.signal`) aborts the session and returns
|
|
74
|
+
`AGENT_ABORTED`.
|
|
75
|
+
|
|
76
|
+
## Local data protection
|
|
77
|
+
|
|
78
|
+
- Session registry: `~/.local/state/claude-opencode-mcp/sessions.json`, written
|
|
79
|
+
with mode `0600` inside a `0700` directory. It contains workspace paths and
|
|
80
|
+
agent output summaries, so treat it as private.
|
|
81
|
+
- Log file: `~/.local/state/claude-opencode-mcp/bridge.log`, mode `0600`,
|
|
82
|
+
rotated at 5 MB. Log fields whose names look like credentials (password,
|
|
83
|
+
secret, token, authorization, API key, credential, private key) are redacted
|
|
84
|
+
before writing.
|
|
85
|
+
- Provider credentials are never stored by the bridge; they stay in OpenCode's
|
|
86
|
+
auth store or environment variables.
|
|
87
|
+
|
|
88
|
+
## Network exposure
|
|
89
|
+
|
|
90
|
+
The started OpenCode server binds to `127.0.0.1` by default and uses a
|
|
91
|
+
configurable port (`0` lets OpenCode pick). If you change `opencode.hostname`
|
|
92
|
+
to a routable address, the OpenCode HTTP API becomes reachable from your
|
|
93
|
+
network: set `OPENCODE_SERVER_PASSWORD` (or `opencode.password`) and firewall
|
|
94
|
+
the port. The bridge only sends credentials to the server you configured.
|
|
95
|
+
|
|
96
|
+
## External OpenCode servers
|
|
97
|
+
|
|
98
|
+
When `opencode.url` is set, the bridge cannot inject agent definitions or
|
|
99
|
+
permissions. It verifies the required agents exist and refuses to delegate
|
|
100
|
+
otherwise. The permissions of those agents are whatever the external server's
|
|
101
|
+
configuration says — review it yourself.
|
|
102
|
+
|
|
103
|
+
Provider credentials are never sent to external URLs from the environment;
|
|
104
|
+
only explicit `username`/`password` or credentials embedded in the URL are
|
|
105
|
+
used.
|
|
106
|
+
|
|
107
|
+
## Known limits
|
|
108
|
+
|
|
109
|
+
State these plainly when deciding what to delegate:
|
|
110
|
+
|
|
111
|
+
1. **Shell access implies write access.** A coder or tester can write files via
|
|
112
|
+
`bash` (`printf > file`, `sed -i`, …). The bridge denies edit tools for the
|
|
113
|
+
tester and forbids git history changes, but shell is powerful. Only delegate
|
|
114
|
+
to shell-enabled agents in repositories you trust.
|
|
115
|
+
2. **`.env` protection covers file tools.** A shell-enabled agent could `cat`
|
|
116
|
+
a protected file through `bash`. This matches OpenCode's own permission
|
|
117
|
+
model, which gates the `read` tool, not `bash`.
|
|
118
|
+
3. **Prompt injection.** Agents read repository content; a hostile repository
|
|
119
|
+
can attempt to influence them. Keep `allowedRoots` tight and review diffs
|
|
120
|
+
with `get_diff` before accepting changes.
|
|
121
|
+
4. **An agent can read anything inside the workspace.** That is intentional:
|
|
122
|
+
hints are not boundaries. Use workspace separation, not hints, to isolate
|
|
123
|
+
code.
|
|
124
|
+
|
|
125
|
+
## Reporting
|
|
126
|
+
|
|
127
|
+
If you find a security issue, please open an issue with a minimal reproduction
|
|
128
|
+
and avoid sharing live credentials.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Start with the log file:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
tail -f ~/.local/state/claude-opencode-mcp/bridge.log
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
and raise the level:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
CLAUDE_OPENCODE_LOG=debug claude
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Error codes
|
|
16
|
+
|
|
17
|
+
| Code | Meaning | Typical fix |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `WORKSPACE_NOT_FOUND` | No usable workspace could be resolved. | Pass `cwd` explicitly or start Claude Code inside the project. |
|
|
20
|
+
| `WORKSPACE_NOT_ALLOWED` | Canonical workspace is outside `workspace.allowedRoots`. | Add the root or clear the list. |
|
|
21
|
+
| `WORKSPACE_NOT_READABLE` | Permission problem accessing the workspace. | Fix directory permissions. |
|
|
22
|
+
| `WORKSPACE_MISMATCH` | OpenCode reported a different directory than requested. | Restart Claude Code; report a bug if it persists. |
|
|
23
|
+
| `INVALID_PATH` | A path hint escaped the workspace, a path had a null byte, or `cwd` was not a directory. | Fix the hint. |
|
|
24
|
+
| `OPENCODE_NOT_AVAILABLE` | Executable not found or not executable. | `npm install -g opencode-ai` or set `OPENCODE_BIN`/`opencode.binary`. |
|
|
25
|
+
| `OPENCODE_START_TIMEOUT` | Server did not print its listening URL in time. | Run `opencode serve` manually; raise `opencode.startupTimeout`; check auth env vars. |
|
|
26
|
+
| `OPENCODE_UNAVAILABLE` | Server died or refused a request. | Retry; check `bridge.log` and `opencode serve` output. |
|
|
27
|
+
| `OPENCODE_SESSION_NOT_FOUND` | Unknown or deleted session id. | Create a new session. |
|
|
28
|
+
| `OPENCODE_ERROR` | OpenCode returned an error (includes provider auth failures). | Read the message; for auth re-run `opencode auth login`. |
|
|
29
|
+
| `AGENT_NOT_FOUND` | Unknown agent, or the external server lacks required agents. | Check `list_agents`; install agents on the external server. |
|
|
30
|
+
| `AGENT_TIMEOUT` | Run exceeded the timeout and was aborted. | Raise `timeouts.execution`/`timeout`, or split the task. |
|
|
31
|
+
| `AGENT_ABORTED` | Run was cancelled. | Expected after `abort_session` or MCP cancellation. |
|
|
32
|
+
| `AGENT_EXECUTION_FAILED` | Provider/API error during the run. | Inspect `details.statusCode`/`responseBody`; retry on `429`. |
|
|
33
|
+
| `MODEL_NOT_AVAILABLE` | No model could be resolved. | `opencode auth login` or set `defaults.model`. |
|
|
34
|
+
| `DIFF_UNAVAILABLE` | No changes were reported and the workspace is not a git repo. | Initialize git for reliable diffs. |
|
|
35
|
+
| `NOT_GIT_REPOSITORY` | A git-only operation was requested outside a repo. | Initialize git. |
|
|
36
|
+
| `CONFIG_INVALID` | Configuration file has wrong types or bad JSON. | Fix the file named in `details.file`. |
|
|
37
|
+
| `INTERNAL_ERROR` | Unexpected bridge error. | Report with the `request_id` from the log. |
|
|
38
|
+
|
|
39
|
+
## The tool call times out in Claude Code
|
|
40
|
+
|
|
41
|
+
- Set `"timeout": 600000` in the `.mcp.json` server entry (per-tool wall clock).
|
|
42
|
+
- `MCP_TOOL_TIMEOUT` raises the default for all servers.
|
|
43
|
+
- Claude Code backgrounds calls after two minutes; this is normal and the
|
|
44
|
+
result still arrives.
|
|
45
|
+
- The stdio idle timeout is 30 minutes with no response *and no progress*; the
|
|
46
|
+
bridge emits progress notifications while polling, so long runs are safe.
|
|
47
|
+
|
|
48
|
+
## OpenCode server fails to start
|
|
49
|
+
|
|
50
|
+
Run the same command the bridge runs, in the workspace:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd /path/to/project
|
|
54
|
+
opencode serve --hostname=127.0.0.1 --port=0
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Common causes:
|
|
58
|
+
|
|
59
|
+
- Port/hostname conflicts: leave `opencode.port` at `0` (choose a free port).
|
|
60
|
+
- `OPENCODE_SERVER_PASSWORD` set in the environment: the server requires basic
|
|
61
|
+
auth. The bridge handles it; if you connect manually, pass credentials.
|
|
62
|
+
- Stale `OPENCODE_CONFIG_CONTENT` with invalid JSON: the bridge ignores
|
|
63
|
+
malformed inherited content and merges its own agents on top.
|
|
64
|
+
- A broken global OpenCode config: run `opencode debug config`.
|
|
65
|
+
|
|
66
|
+
## DeepSeek is not detected by `init`
|
|
67
|
+
|
|
68
|
+
`claude-opencode init` runs `opencode models` and looks for `deepseek/*`. If the
|
|
69
|
+
list is empty:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
opencode auth login
|
|
73
|
+
opencode models | grep deepseek
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Agents are missing on an external server
|
|
77
|
+
|
|
78
|
+
`AGENT_NOT_FOUND` with `opencode.url` set means the server has no definitions
|
|
79
|
+
for `deepseek-*`. Either:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
claude-opencode init
|
|
83
|
+
mkdir -p .opencode/agents
|
|
84
|
+
cp .claude-opencode/agents/*.md .opencode/agents/
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
then restart the external server, or remove `opencode.url` and let the bridge
|
|
88
|
+
manage the server.
|
|
89
|
+
|
|
90
|
+
## A run seems stuck
|
|
91
|
+
|
|
92
|
+
- Check `/tasks` in Claude Code for a backgrounded call.
|
|
93
|
+
- `get_session` returns live `opencode.status` (`busy`, `idle`, `retry`).
|
|
94
|
+
- `abort_session` stops it.
|
|
95
|
+
- The bridge auto-rejects permission prompts it observes, and every run has a
|
|
96
|
+
timeout, so a run cannot wait forever.
|
|
97
|
+
|
|
98
|
+
## Provider errors during runs
|
|
99
|
+
|
|
100
|
+
Results include `error.details` from OpenCode, for example:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"status": "failed",
|
|
105
|
+
"error": {
|
|
106
|
+
"code": "AGENT_EXECUTION_FAILED",
|
|
107
|
+
"message": "rate limited",
|
|
108
|
+
"details": { "statusCode": 429, "isRetryable": true }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Retry by sending a follow-up (`send_message`) on the same session or delegating
|
|
114
|
+
again.
|
|
115
|
+
|
|
116
|
+
## Diagnostics
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
claude-opencode-mcp --version
|
|
120
|
+
claude-opencode init # validates binary, provider, workspace, MCP config
|
|
121
|
+
claude mcp list # shows connection health
|
|
122
|
+
node dist/index.js --version
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
When reporting a bug, include the `request_id` from the error payload and the
|
|
126
|
+
matching lines from `bridge.log`.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Workspace model
|
|
2
|
+
|
|
3
|
+
The core invariant: **a delegated agent operates against the same intended
|
|
4
|
+
workspace as Claude Code, and can inspect it directly through OpenCode's
|
|
5
|
+
tools.** No repository copying, no temporary clones for normal operation.
|
|
6
|
+
|
|
7
|
+
## Resolution
|
|
8
|
+
|
|
9
|
+
For every tool call the workspace is resolved in this order:
|
|
10
|
+
|
|
11
|
+
1. explicit `cwd` argument
|
|
12
|
+
2. `CLAUDE_PROJECT_DIR` (set by Claude Code for stdio MCP servers)
|
|
13
|
+
3. the MCP server process working directory
|
|
14
|
+
4. `workspace.defaultCwd` from configuration
|
|
15
|
+
|
|
16
|
+
Candidates 1 and 2 are *required*: if one of them is present but invalid
|
|
17
|
+
(`WORKSPACE_NOT_FOUND`, `WORKSPACE_NOT_READABLE`, `WORKSPACE_NOT_ALLOWED`,
|
|
18
|
+
`INVALID_PATH`), the call fails immediately. The bridge never silently falls
|
|
19
|
+
back to a different directory for an explicitly requested workspace. Only the
|
|
20
|
+
process directory and `defaultCwd` are best-effort fallbacks.
|
|
21
|
+
|
|
22
|
+
If no candidate can be validated, the error is:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
WORKSPACE_NOT_FOUND: Unable to determine project workspace. Provide cwd explicitly.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Validation
|
|
29
|
+
|
|
30
|
+
Before a session is created:
|
|
31
|
+
|
|
32
|
+
- the path must exist, be a directory, and be readable/executable;
|
|
33
|
+
- it is canonicalized with `realpath` (symlinks and `..` are resolved);
|
|
34
|
+
- when `workspace.allowedRoots` is configured, the canonical path must live
|
|
35
|
+
inside one of the canonical roots — anything else is rejected with
|
|
36
|
+
`WORKSPACE_NOT_ALLOWED`;
|
|
37
|
+
- the git root is detected (`git rev-parse --show-toplevel`) for diff
|
|
38
|
+
baselines and reported as `gitRoot`.
|
|
39
|
+
|
|
40
|
+
Path traversal (for example `cwd: "../../etc"` or a `paths` hint escaping the
|
|
41
|
+
workspace) is rejected with `INVALID_PATH`.
|
|
42
|
+
|
|
43
|
+
## Path hints
|
|
44
|
+
|
|
45
|
+
`delegate_task` accepts `paths`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"task": "Review authentication",
|
|
50
|
+
"paths": ["src/auth", "src/middleware/auth.ts", "tests/auth"]
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Hints are turned into absolute paths, must stay inside the workspace, and are
|
|
55
|
+
inserted into the task message as starting points. They are **not** a
|
|
56
|
+
filesystem boundary: the agent is explicitly told to inspect additional files
|
|
57
|
+
and to not assume everything relevant lives under the hints.
|
|
58
|
+
|
|
59
|
+
## One server per workspace
|
|
60
|
+
|
|
61
|
+
The bridge starts one OpenCode server per workspace directory and runs it with
|
|
62
|
+
that directory as its working directory. Sessions for that workspace reuse it.
|
|
63
|
+
`opencode.maxServers` (default 4) bounds how many idle servers are kept; the
|
|
64
|
+
least recently used idle server is stopped when the limit is exceeded.
|
|
65
|
+
|
|
66
|
+
During the first call for a workspace the bridge asks the server
|
|
67
|
+
(`GET /path`) which directory it is serving and compares it with the requested
|
|
68
|
+
canonical path. A mismatch aborts with `WORKSPACE_MISMATCH` rather than
|
|
69
|
+
delegating into the wrong repository.
|
|
70
|
+
|
|
71
|
+
## The workspace contract
|
|
72
|
+
|
|
73
|
+
Internally every run carries:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
interface AgentWorkspace {
|
|
77
|
+
cwd: string; // canonical absolute directory
|
|
78
|
+
requestedPath: string;
|
|
79
|
+
allowedPaths?: string[]; // reserved; hints are not restrictions
|
|
80
|
+
deniedPaths?: string[]; // reserved; sensitive patterns are enforced via permissions
|
|
81
|
+
gitRoot?: string;
|
|
82
|
+
readOnly: boolean; // derived from the agent profile
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
and the agent message always contains:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
Workspace:
|
|
90
|
+
/abs/path
|
|
91
|
+
|
|
92
|
+
Task:
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
Relevant paths (starting points only, not a boundary):
|
|
96
|
+
- /abs/path/src/auth
|
|
97
|
+
|
|
98
|
+
Permissions:
|
|
99
|
+
- file edits: denied
|
|
100
|
+
- shell commands: denied
|
|
101
|
+
- read-only agent: yes
|
|
102
|
+
|
|
103
|
+
Instructions:
|
|
104
|
+
- Work inside the specified workspace; do not read or write outside it.
|
|
105
|
+
- Inspect additional files and directories whenever that helps the task.
|
|
106
|
+
- Do not assume relevant code exists only under the supplied path hints.
|
|
107
|
+
...
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Demo: two repositories
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
repo-a/
|
|
114
|
+
ONLY_A.txt
|
|
115
|
+
repo-b/
|
|
116
|
+
ONLY_B.txt
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Delegating to each with the same task ("list the root .txt file and report its
|
|
120
|
+
contents") returns `alpha-42` for repo-a and `beta-99` for repo-b. This scenario
|
|
121
|
+
is covered by the end-to-end test suite (`tests/e2e/real-opencode.test.ts`).
|
|
122
|
+
|
|
123
|
+
## Choosing a workspace without Claude Code
|
|
124
|
+
|
|
125
|
+
Any MCP client can pass `cwd` explicitly. Claude Code additionally provides
|
|
126
|
+
`CLAUDE_PROJECT_DIR`, which makes the common case zero-configuration.
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-opencode-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP bridge that lets Claude Code delegate software-engineering tasks to OpenCode agents (DeepSeek by default) inside the same workspace.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "claude-opencode-mcp contributors",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/putuandy/claude-opencode-mcp.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/putuandy/claude-opencode-mcp#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/putuandy/claude-opencode-mcp/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"opencode",
|
|
21
|
+
"deepseek",
|
|
22
|
+
"coding-agent"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"claude-opencode-mcp": "./dist/index.js",
|
|
29
|
+
"claude-opencode": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"agents",
|
|
42
|
+
"docs",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE",
|
|
45
|
+
"CHANGELOG.md",
|
|
46
|
+
"SECURITY.md"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p tsconfig.build.json",
|
|
50
|
+
"prepare": "npm run build",
|
|
51
|
+
"prepack": "npm run build",
|
|
52
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
53
|
+
"lint": "biome check .",
|
|
54
|
+
"lint:fix": "biome check --write .",
|
|
55
|
+
"format": "biome format --write .",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest",
|
|
58
|
+
"test:e2e": "npm run build && cross-env CLAUDE_OPENCODE_E2E=1 vitest run --config vitest.e2e.config.ts",
|
|
59
|
+
"dev": "tsx src/index.ts"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
63
|
+
"@opencode-ai/sdk": "^1.18.32",
|
|
64
|
+
"commander": "^14.0.3",
|
|
65
|
+
"yaml": "^2.9.1",
|
|
66
|
+
"zod": "^4.6.5"
|
|
67
|
+
},
|
|
68
|
+
"optionalDependencies": {
|
|
69
|
+
"opencode-ai": "^1.18.32"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@biomejs/biome": "^2.5.14",
|
|
73
|
+
"@types/node": "^26.6.2",
|
|
74
|
+
"cross-env": "^10.1.0",
|
|
75
|
+
"tsx": "^4.23.15",
|
|
76
|
+
"typescript": "^5.9.3",
|
|
77
|
+
"vitest": "^4.1.11"
|
|
78
|
+
}
|
|
79
|
+
}
|