OpenCode takes a plain-JSON, no-magic approach to MCP configuration — there's no opencode mcp add wizard like Claude Code's CLI, you edit opencode.json (or ~/.config/opencode/opencode.json for a global config) directly. That's a small amount of extra friction on setup, and a large amount of extra clarity once it's running, because the entire server config is one file you can diff, review, and check into git.
Installing and Connecting GitHub MCP to OpenCode
OpenCode supports both local (stdio, spawns a process) and remote (HTTP/SSE) MCP server definitions in its config schema. For GitHub MCP, the remote hosted endpoint is again the lowest-friction option:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"github": {
"type": "remote",
"url": "https://api.githubcopilot.com/mcp/",
"enabled": true
}
}
}
For a local stdio setup with the official Go binary (no Docker), point OpenCode at the binary directly and pass the PAT as an env var in the server definition:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"github": {
"type": "local",
"command": ["github-mcp-server", "stdio"],
"environment": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "{env:GITHUB_PAT}",
"GITHUB_TOOLSETS": "repos,issues,pull_requests"
},
"enabled": true
}
}
}
The {env:GITHUB_PAT} interpolation pulls from your shell environment at launch time rather than hardcoding the token in the JSON file — this is the pattern to use for any config you intend to commit to a repo.
Confirm the connection from inside a session:
opencode
/mcp
If github shows as failed to connect with a local server, the most common cause is the binary not being on PATH inside the environment OpenCode spawns from — test it standalone first:
which github-mcp-server
GITHUB_PERSONAL_ACCESS_TOKEN=$GITHUB_PAT github-mcp-server stdio
Tips
- Use{env:VAR_NAME}interpolation for tokens inopencode.json— never hardcode a PAT into a file you might commit.
- Test thegithub-mcp-serverbinary standalone before wiring it into OpenCode; a silent connection failure inside the TUI is much harder to debug than a binary that errors immediately on its own.
- Project-levelopencode.json(repo root) overrides the global~/.config/opencode/opencode.json— check both if a server behaves differently than you expect between projects.
Managing Issues, Pull Requests, and Repos from OpenCode
Once connected, OpenCode's model sees the same GitHub MCP tool surface as any other client — tool names typically show up prefixed as github_list_issues, github_create_pull_request, etc. (exact prefixing depends on OpenCode's tool-naming convention for the configured server key). Natural-language prompts work the same way:
> show me all pull requests in this repo that have been open for more than 5 days
with no reviews
> read PR #77's diff and tell me if the new endpoint added in api/routes.go
has a corresponding entry in api/openapi.yaml
For repo-content tasks that don't need a local clone — reading a config file from a different branch, or checking what a file looked like at a specific commit — get_file_contents avoids the checkout entirely:
> get the contents of config/feature-flags.yml from the release/2.4 branch
and diff it conceptually against what's on main
OpenCode's permission model is coarser than Claude Code's per-tool allow/ask/deny: by default it prompts for any tool call it classifies as a write, based on the tool's declared behavior in the MCP server's schema (the official github-mcp-server marks its write tools accordingly). You can pre-approve specific tools per-session via the TUI's permission prompt ("always allow this tool"), but there's no persisted fine-grained config file equivalent to Claude Code's settings.json allow-lists as of the current OpenCode release — treat every session as starting permission-neutral unless you've set "enabled": false on toolsets you never want available at all (done by omitting them from GITHUB_TOOLSETS in the local-server config, since there's no separate allow/deny layer on top).
> create an issue titled "Rate limiter drops requests under burst load"
with a description summarizing the attached stack trace, label it "bug"
This triggers a permission prompt in the TUI before create_issue fires — approve, deny, or approve-and-remember-for-session.
Tips
- Because OpenCode's permission granularity is session-level rather than a persisted per-tool config file, be more conservative about what toolsets you even load — restrict at theGITHUB_TOOLSETSlevel rather than relying on prompts to catch mistakes.
-get_file_contentsagainst a branch or commit SHA is the fastest way to answer "what does X look like on Y" questions without a local checkout — use it before reaching for a fullgit fetch.
- Session-level "always allow" approvals reset when you start a new OpenCode session — don't expect prior approvals to persist across restarts.
Practical Example: AI-Assisted Issue Triage in OpenCode
A concrete, repeatable workflow: an inbox of untriaged issues, and you want labels, priority, and a duplicate check, without touching each one by hand.
> list all issues with no labels, opened in the last 14 days.
For each one:
1. read the full body
2. search open+closed issues for likely duplicates using search_issues
3. propose a label from this set: bug, feature, question, docs, duplicate
4. propose a priority: P0, P1, P2
Show me the full list as a table before you apply anything.
OpenCode will chain list_issues → per-issue get_issue → search_issues for duplicate detection → produce a table. Reviewing the table before approving each update_issue/add_issue_comment call is the checkpoint that keeps this safe — the duplicate-detection step in particular is heuristic (keyword-based search, not semantic), so it will occasionally flag a false match on generic titles like "Login fails" across unrelated issues.
> apply the labels for rows 1, 2, and 4 from that table.
For row 3, don't apply — I want to check that duplicate manually first.
This selective-approval pattern — reviewing a proposed batch, then explicitly naming which rows to execute — is the difference between issue triage that saves real time and issue triage that creates a new cleanup job of mislabeled issues.
For a duplicate flagged with reasonable confidence, closing with a reference comment:
> close issue #341 as a duplicate of #298, and add a comment linking to #298
explaining briefly why
Tips
- Treatsearch_issues-based duplicate detection as a first-pass filter, not a verdict — it's keyword matching against title/body, not semantic similarity, and generic issue titles produce false positives regularly.
- Always route batch triage through a "show me the table, then tell me which rows to apply" checkpoint — applying an entire proposed batch in one shot is how you end up with ten issues mislabeled the same wrong way.
- When closing as duplicate, always ask for a comment linking the canonical issue — an unexplained close reads as dismissive to whoever filed it, human-triaged or not.
Known Limitations for GitHub MCP in OpenCode
A few gaps worth knowing before you build a workflow around this combination, rather than discovering them mid-task:
- No persisted per-tool permission config. As covered above, OpenCode's approval model is session-scoped. If you want Claude Code-style "always auto-allow reads, always ask on writes" behavior that survives restarts, you'll be re-approving read tools every session, or restricting toolsets at the config level to compensate.
- SSE/remote transport stability. The remote HTTP server works well for short sessions but long-running OpenCode sessions (multi-hour, doing extended triage) have occasionally needed a manual
/mcpreconnect after the underlying SSE connection idles out — a known class of issue with SSE-based MCP transports generally, not GitHub-specific, but you'll see it here first if you run OpenCode for extended stretches. - Tool-name prefixing varies with server key naming. If you name your config key something other than
github(e.g.,gh-work), the exposed tool names shift with it, which can break prompts you copy from documentation assuming the defaultgithubkey. Keep the config key asgithubunless you have a specific reason not to. - No built-in diff-rendering UI. Unlike the Claude Code VS Code extension's inline diff cards, OpenCode's TUI renders tool call arguments and results as text — readable, but you're reading a JSON-ish PR diff in a terminal pane rather than a proper side-by-side view. For heavy visual PR review, this course's Cursor topic (Topic 5) is the better fit.
- No GitHub App auth path documented for the local server config as of current OpenCode releases — the
environmentblock expects a PAT-style token. If your org mandates GitHub App-based access, you'll need to front it with a short-lived-token-minting wrapper script rather than pointing OpenCode's config directly at app credentials.
Tips
- If a long OpenCode session's GitHub tools suddenly stop responding, run/mcpand reconnect before assuming the token expired — it's often just an idled SSE connection.
- Keep the MCP config key namedgithub— tool-name prefixes derive from it, and mismatched prefixes in copied prompts are a common source of "the tool doesn't exist" confusion.
- For GitHub App-based org policies, wrap token minting in a small script that refreshes the installation token and exports it asGITHUB_PERSONAL_ACCESS_TOKENbefore launching OpenCode, since there's no native app-auth field in the local server config.
Tips
Tips
- Configure GitHub MCP inopencode.jsonwith{env:VAR}interpolation for tokens — never a literal PAT in a file you might commit.
- OpenCode's permission model is session-scoped, not persisted per-tool — compensate by restrictingGITHUB_TOOLSETSup front rather than relying on approval prompts alone.
- For heavy visual PR review work, pair OpenCode's fast triage loop with a client that renders diffs visually (Cursor or the Claude Code VS Code extension) rather than reading raw diffs in a terminal pane.