Skip to content

feat(mcp): collect OpenCode MCP server configs - #187

Open
raysubham wants to merge 1 commit into
step-security:mainfrom
raysubham:feat/opencode-mcp-coverage
Open

feat(mcp): collect OpenCode MCP server configs#187
raysubham wants to merge 1 commit into
step-security:mainfrom
raysubham:feat/opencode-mcp-coverage

Conversation

@raysubham

Copy link
Copy Markdown
Contributor

What

OpenCode servers were invisible to the MCP inventory. Three things about its config kept it out of every existing path:

  • servers live under a top-level mcp key, not mcpServers;
  • the config is accepted under two basenames, opencode.json and opencode.jsonc;
  • OpenCode's own documented examples carry comments and trailing commas.

This reads all of it.

Global config ~/.config/opencode/opencode.{json,jsonc} — known exact paths, same layout on every platform
Project config found by the existing bounded walk, via two new recognized basenames
Vendor label basename match, ordered ahead of the substring cases
Wire key mcp preserved as-is — this pipeline filters fields, it never rewrites another vendor's schema

Both global paths resolve against the home the detector was given, never the process environment, which on Windows belongs to the service account. Both spellings route through hujson.Standardize, which handles the trailing commas the comment stripper alone does not.

Secrets

Unchanged and deny-by-default. The allowlist is still command / args / serverUrl / url, so OpenCode's environment and headers blocks are never collected, and a parse failure still emits empty content rather than raw bytes. Byte-exact filter tests and forbidden-key assertions cover both.

The one behavioural guard worth reviewing

internal/detector/mcp.go — a value under mcp that is not a map of servers is skipped, not emitted empty:

if servers, ok := raw["mcp"]; ok {
    if filtered := filterServerFields(servers); filtered != nil {
        result["mcp"] = filtered
        found = true
    }
}

Without it, {"mcpServers":{...},"mcp":true} emits {"mcp":null,"mcpServers":{...}}, the backend rejects the whole document (mcp is not an object), and the valid mcpServers beside it is dropped. mcp is an ordinary enough word to show up as a scalar flag, so that regression would reach every walked .mcp.json, not just OpenCode's files.

Scoped deliberately:

  • "mcp": null is left emitting {"mcp":{}}. null is the one non-map json.Unmarshal accepts into a map type, so it decodes to an empty set, not a filter failure. An empty object parses backend-side and evicts nothing, and it is exactly what {"mcpServers":null} has always done — tightening only mcp would make it the odd key out. Pinned byte-exact in both directions.
  • mcpServers / context_servers / servers are untouched. Same latent shape, but pre-existing and not introduced here.

Tests

  • Scalar mcp values (true, "enabled", ["a"], 42): siblings survive, and a lone scalar fails closed with no content.
  • Vendor-label ordering pinned by cursor-tools/, claude-tools/ and .vscode/ rows — each would be claimed by a later substring case if the basename case did not come first.
  • Negative rows: .mcp.json inside an opencode checkout stays Discovered, as does opencode.json.bak.
  • Home resolution asserted on all three GOOS.

Mutation-checked rather than assumed: reverting the nil guard reproduces the exact bad wire shape, moving the basename case last flips all three ordering rows, and weakening the guard to len(filtered) > 0 flips the null assertion.

Docs

README.md, SCAN_COVERAGE.md (global + project rows) and docs/mcp-audit.md, which now names OpenCode's environment alongside env as stripped. The MCP table's first column widened 16 → 18 to fit OpenCode (project); the 11 sibling rows are whitespace-only.

Not fixed here: docs/mcp-audit.md:75 still claims non-JSON configs are included as-is, which is false today and predates this change. Separate fix.

Not stacked on #186

#186 (credential inventory) also touches OpenCode config parsing. This branch is independently based on main and shares no files with it, so either can merge first. Once both land, the literal DetectKnownUserConfigs assertion belongs in a follow-up.

Verification

gofmt clean · go vet 0 · go test ./... -race -count=142 packages ok, 0 failures, 0 data races · golangci-lint run ./...0 issues · make smoke44/44 · CGO_ENABLED=0 builds green for linux/amd64, darwin/arm64, windows/amd64.

No version or changelog bump — that is the release cut's job.

End-to-end coverage is in a companion integration-test PR, held until this ships in a release.

OpenCode was invisible to the MCP inventory. It declares servers under a
top-level "mcp" key rather than "mcpServers", accepts its config under two
basenames (opencode.json and opencode.jsonc), and its own documented
examples carry comments and trailing commas — so nothing about it was
readable by the existing paths.

- Read ~/.config/opencode/opencode.{json,jsonc} as known exact paths. The
  layout is identical on every platform, and both are resolved against the
  home the detector was given rather than the process environment, which
  on Windows belongs to the service account.
- Recognize both basenames in the bounded walk. That covers project-level
  configs — which OpenCode searches for upward from the working directory
  — without any project-root logic or new walk root of its own.
- Label by basename ahead of the substring vendor cases, so a config under
  a directory named cursor/ or claude/ is still reported as OpenCode.
- Route both spellings through the JSONC path, since hujson.Standardize
  handles the trailing commas that the comment stripper alone does not.

Secrets are unaffected: the field allowlist stays deny-by-default, so
OpenCode's environment and headers blocks are never collected.

A value under "mcp" that is not a map of servers is skipped rather than
emitted empty. "mcp" is an ordinary enough word to appear as a scalar flag
in a config whose real servers live under mcpServers, and emitting a null
for it would make the backend reject the whole document and drop the valid
servers beside it — a regression that would reach every walked .mcp.json,
not only OpenCode's own files.
@raysubham raysubham mentioned this pull request Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant