🌐 Website: backendarchitect.github.io/gospect-mcp
A Go-only, report-first code scanner exposed as an MCP server. It indexes a Go module, runs deterministic analyzers, and reports genuine bugs, dead code, stale docs and outdated APIs. It never modifies your code — fixes are a separate, explicitly-invoked step.
Think of it as an MRI for your Go code: a deep, non-invasive scan that produces a diagnosis your editor or AI agent acts on — not a tool that silently rewrites things.
Install the latest version straight from source (needs Go 1.21+):
go install github.com/backendArchitect/gospect-mcp@latestThis drops a gospect-mcp binary in your $GOBIN (usually ~/go/bin — make sure it's on your
PATH). Or grab a prebuilt binary (Linux/macOS/Windows, amd64/arm64) from the
Releases page. Then scan any Go module:
gospect-mcp scan /path/to/your/module ./...You'll get a JSON report of findings, ordered by importance — bugs first, then medium, then low.
That's it — no config, no services, no code changes. Run gospect-mcp help for every command and flag.
Progress (per-module load + a summary) streams to stderr by default, so a long scan never looks
hung — and the JSON on stdout stays clean for piping. Silence it with -quiet:
gospect-mcp scan /path/to/monorepo # progress on stderr, JSON on stdout
gospect-mcp scan -quiet /path/to/monorepo > report.json # stderr silentA big repo can surface hundreds of findings. Narrow the output — the same flags work on scan and
check:
gospect-mcp scan -format text -min-severity high . # readable, only the serious stuff
gospect-mcp scan -category bug . # just bugs
gospect-mcp scan -detector nilness,unmarshal . # specific detectors
gospect-mcp scan -exclude '*.pb.go,mocks/' . # skip generated code / mocks-min-severity low|medium|high— drop everything below the given severity.-category a,b/-detector a,b— keep only those categories/detectors.-exclude glob,glob— drop findings whose file path matches a glob or substring.-format text(scan) — a readable grouped listing instead of JSON.
Speed & gotchas.
scanrequires a path —gospect-mcp scanalone starts the MCP server (it waits on stdin, which can look like a hang). A whole large module scans in a few seconds (e.g. ~270 packages in ~5s), but the first scan of a big repo may take longer while Go compiles its dependencies once — subsequent scans are fast. Use-verboseto see progress.
Point scan at the repo root and it finds every nested go.mod and scans them all in one
run — no need to loop over services by hand:
gospect-mcp scan ./my-monorepo # scans the root module + every nested service module(Multi-module discovery kicks in only for the default ./... pattern; pass an explicit pattern to
scope to a single module.) If one module can't be loaded — stale vendoring, a private dependency —
it's skipped with a reason printed to stderr and listed in the report's skipped_modules, and
the other modules still scan. A partial scan never masquerades as full coverage.
gospect-mcp is Go-only. Point it at a repo with no Go and it says so — and names what it looks
like instead (… looks like a JavaScript/TypeScript (Node/React) project. gospect-mcp is Go-only.)
rather than failing cryptically.
Mark deliberate code with a //gospect:ignore comment on the flagged line (or the line directly
above it), mirroring the familiar //nolint idiom:
resp.Body.Close() //gospect:ignore // suppress any finding on this line
_ = risky() //gospect:ignore unchecked-error // suppress only these detectors (comma/space separated)Suppressed findings drop from the report; the count is reported as suppressed. To silence a whole
detector across a run instead, use check -ignore <detector,...>.
For repo-wide, checked-in suppression, add a .gospectignore at the module root:
# .gospectignore — one rule per line
*.pb.go # skip generated protobuf
mocks/ # skip a whole directory (path glob or substring)
detector:todo # never report TODO markers
detector:go-version # we pin Go deliberatelyIt's honored by both the CLI and the MCP server; the count is reported as ignored.
Generated code is skipped automatically. Files carrying the standard // Code generated … DO NOT EDIT. marker (protobuf, mocks, stringer, …) never contribute findings — the fix belongs in the
generator, not its output. The count is reported as generated; pass -include-generated to scan
them anyway.
An existing codebase can surface hundreds of findings. Snapshot them once, then only see — or gate on — what's new:
gospect-mcp scan . > gospect-baseline.json # snapshot today's findings (commit this)
gospect-mcp scan -baseline gospect-baseline.json . # later: shows only NEW findings
gospect-mcp check -baseline gospect-baseline.json -fail-on medium . # CI fails only on new onesMatching is by a line-independent fingerprint (detector + path + message), so a pre-existing finding that merely shifts lines stays baselined.
By default gospect runs a fast, high-precision analyzer set. Add -staticcheck to also run the
staticcheck SA analyzers — the canonical Go bug checks (SA1019
deprecated-API usage, dead assignments, impossible conditions, and ~100 more):
gospect-mcp scan -staticcheck . # much deeper bug detection
gospect-mcp check -staticcheck -since origin/main . # deep, but only on the PR's changes (fast)It's opt-in because it's thorough but slow — roughly an order of magnitude slower than the
default set on a large module. Pairing it with -since (diff mode) keeps PR checks fast while still
getting staticcheck depth on the changed code. Findings land under the bug category (and
modernize for deprecated-API SA1019), so existing severity gates and filters apply.
On a pull request you don't need to rescan the whole repo — only what changed. -since <git-ref>
loads and scans just the packages containing .go files changed since that ref:
gospect-mcp scan -since origin/main . # scan only changed packages
gospect-mcp check -since origin/main -fail-on high . # gate a PR on its own changesOn a large monorepo this turns a ~30s full scan into a ~1–2s PR check (it loads only the
touched module, not every service). Use origin/main... (three dots) for merge-base semantics on a
branch. Outside a git repo, or without -since, it does a normal full scan. Diff mode skips the
whole-repo graph detectors, since a PR check should only surface findings in the code it touched.
gospect-mcp scan -format sarif . > gospect.sarif # upload via github/codeql-action/upload-sarif
gospect-mcp scan -vuln . # also run govulncheck for known-CVE deps-format sarif emits SARIF 2.1.0 so findings appear as inline PR annotations. -vuln is opt-in
(it's slow and needs the vulnerability database); if govulncheck isn't installed it says so
instead of failing.
Prefer building it yourself? See From source.
gospect-mcp version # print the installed version
gospect-mcp update # check GitHub for a newer release; update if one exists, else "up to date"
gospect-mcp uninstall # remove the installed binary (asks to confirm; add --yes to skip)update checks the latest GitHub release and, when a newer one exists, reinstalls via
go install …@<tag>. If no release is newer it prints that you're up to date; if none are
published yet it says so.
uninstall deletes the running binary from disk (it resolves its own path, so it also works for
a downloaded binary). It won't touch a go run temp build. After removing, delete the gospect
entry from your MCP client config and any GOSPECT_GRAPH_* env vars.
- Report-first. The default output is a report. It will not touch your code. Fixes only happen when you explicitly ask for them.
- Sensor, not oracle. The server runs pure Go tooling and emits findings with evidence. It uses no LLM and detects no installed AI — the MCP host you connect it to (Claude Code, Cursor, any MCP client) supplies the intelligence. That makes it model- and vendor-agnostic.
- Genuine over noisy. It builds on the real Go toolchain (
go/packages,go/analysis, SSA) so candidates are semantically backed, not grep guesses. - Universal. Works on any Go module where
go build ./...succeeds — single- or multi-module.
gospect-mcp speaks the Model Context Protocol over stdio, so any MCP client can use it — no
plugin, no adapter. And because every scan is stateless (nothing persists between calls, no
shared index, no background daemon), you can point as many agents at it as you like with nothing to
coordinate. One binary, any number of assistants.
Claude Code
claude mcp add gospect gospect-mcpAny other MCP client — Cursor, Windsurf, Cline, VS Code (Continue/Copilot), Zed, Codex CLI,
Gemini CLI, Claude Desktop — takes the same stdio config (~/.claude.json, a project .mcp.json,
or the client's own MCP settings):
{
"mcpServers": {
"gospect": {
"command": "gospect-mcp",
"args": []
}
}
}| Surface | How it connects | Notes |
|---|---|---|
| Claude Code | claude mcp add gospect gospect-mcp |
one-liner |
| Cursor / Windsurf / Cline | mcpServers JSON above |
stdio |
| VS Code (Continue, Copilot MCP) | mcpServers JSON above |
stdio |
| Zed / Codex CLI / Gemini CLI | client's MCP config | stdio |
| Claude Desktop | claude_desktop_config.json |
stdio |
Then ask your agent to scan a module. It calls the scan tool and reasons over the report — and,
because the server is report-only, it can't change your code unless you explicitly ask (via the
separate propose_fix tool, which still only emits guidance).
gospect builds on the Go type-checker, so its runtime is bounded by go build — not by the tool.
Warm-cache, single machine:
| Scope | Packages | Time | Notes |
|---|---|---|---|
| Single small module | ~12 | ~1s | load ~0.9s / scan ~0.1s |
| Mid-size module | 272 | ~2.5s | load ~2.1s / scan ~0.5s |
| Full 9-module monorepo | 458 | ~26s | modules load in parallel |
The first scan of a big repo is slower while Go compiles its dependencies once; subsequent scans
are fast. Monorepo modules load concurrently. Scope with a package pattern (./somepkg/...) for
instant results, and use -verbose to watch progress on a long run.
All detectors are deterministic and report-only. Findings carry category, detector, severity,
a confidence (how sure it's real — SSA/type-checked checks are high, heuristic markers medium/
low), file:line, and message; the report includes by_category and by_severity summaries.
Filter noisy heuristics with -min-confidence high.
| Category | Detectors |
|---|---|
| bug | nilness (SSA nil-deref), lostcancel (leaked context.CancelFunc), bodyclose (unclosed HTTP body), httpresponse, unmarshal, copylock, errorsas, nilfunc, unreachable, ineffassign (dead assignment) |
| missing | unimplemented stubs (panic("not implemented")), TODO/FIXME markers, unchecked error returns (errcheck-lite) |
| modernize | outdated go.mod go directive, loopclosure (pre-1.22 loop-var capture) |
| over-engineered | high-complexity — functions whose cyclomatic or cognitive complexity exceeds conservative thresholds (built-in, no external graph) |
The default set is built entirely on golang.org/x/tools. Opt into the deeper
staticcheck SA analyzers with -staticcheck, and exported-but-untested
functions with -untested (both below).
Some detectors need whole-repo relationships single-package analysis can't see. gospect builds a built-in graph from the loaded packages for the two that don't need a full call graph:
- over-engineered / high-complexity — runs by default; cyclomatic or cognitive complexity over conservative thresholds. No configuration.
- untested-exports — opt-in with
-untested(exported functions with no test in the same package). The built-in check is name-based and noisy on large repos, so it's off by default. - stale-doc / swagger-drift — runs automatically when an OpenAPI/Swagger spec is present and
routes are found in the code. gospect extracts registered routes from the AST (net/http
Handle/HandleFunc, and chi/gin/echo verb methods liker.GET("/x", …)) and flags documented endpoints with no matching route. If no routes are detected it stays silent (no false positives).
The remaining route detector, unhandled-route (a route declared with no handler), needs a real call/route graph. gospect gets that by acting as an MCP client of a code-intelligence graph such as codebase-memory-mcp — no graph of its own, no duplicated index. An external graph also makes untested-exports and swagger-drift more accurate (real TESTS/HANDLES edges instead of heuristics).
Enable it with three env vars (all optional; unset = external graph disabled, built-in detectors still run):
export GOSPECT_GRAPH_CMD="codebase-memory-mcp" # command to launch the graph MCP server
export GOSPECT_GRAPH_PROJECT="my-project" # project name to query
export GOSPECT_GRAPH_SCOPE="internal/" # optional file-path substring to scope queries
gospect-mcp scan /path/to/moduleWhen configured, the report additionally gains the route-based findings:
- unhandled-route — HTTP routes registered with no handler.
- stale-doc / swagger-drift — endpoints documented in an OpenAPI/Swagger spec (JSON or YAML) with no matching registered route (heuristic path matching; report-first).
A graph connection failure never fails the scan; it's recorded in the report's graph_error
field and the local findings are still returned.
Input
| field | type | required | default | description |
|---|---|---|---|---|
path |
string | ✅ | — | filesystem dir of the Go module |
patterns |
string[] | ["./..."] |
package patterns to scan |
Output — a JSON Report: load stats + a flat, sorted list of Findings. No mutations.
gospect-mcp scan ./testdata/buggy{
"path": "./testdata/buggy",
"packages_loaded": 1,
"load_errors": 0,
"load_millis": 8,
"finding_count": 5,
"by_category": { "bug": 1, "missing": 3, "modernize": 1 },
"findings": [
{
"category": "bug",
"detector": "nilness",
"severity": "high",
"file": ".../buggy.go",
"line": 8,
"message": "nil dereference in load"
},
{
"category": "missing",
"detector": "unchecked-error",
"severity": "medium",
"file": ".../stubs.go",
"line": 14,
"message": "error return value is not checked"
}
]
}gospect-mcp check scans and exits non-zero when any finding is at or above a severity —
so a PR fails if it introduces (say) a nil dereference.
gospect-mcp check -fail-on high . # exit 1 if any high-severity finding
gospect-mcp check -fail-on medium -ignore todo,go-version ./...
gospect-mcp check -format json . # machine-readableFlags may appear anywhere (before, after, or between the path/patterns). Exit codes: 0 clean,
1 blocking findings, 2 error.
Drop-in GitHub Action:
- uses: backendArchitect/gospect-mcp@v1
with:
path: .
fail-on: high # high | medium | low
# ignore: todo,go-versionPost findings as code-scanning annotations (SARIF). Set sarif: true and grant the job
security-events: write — findings then show up inline on the PR and in the Security tab. SARIF is
generated and uploaded before the gate, so annotations appear even when the check fails:
permissions:
contents: read
security-events: write # required for SARIF upload
steps:
- uses: actions/checkout@v4
- uses: backendArchitect/gospect-mcp@v1
with:
path: .
sarif: true # generate + upload SARIF
fail-on: high # still gate the job; set gate: false to annotate onlyOr run it directly:
- run: |
go install github.com/backendArchitect/gospect-mcp@latest
gospect-mcp check -fail-on high .Fixes are opt-in and separate from scanning. propose_fix takes a finding and returns a fix
envelope — it never edits code:
root_cause,expected_scope, and areuse_hint(reuse before adding)- a
verify_firstchecklist led by an adversarial "default to not a real issue" prompt - ponytail
constraints(smallest root-cause fix, no unrequested abstractions, one runnable check)
The calling agent uses the envelope to make a minimal, verified fix. CLI form:
echo '{"detector":"unchecked-error","file":"x.go","line":14,"message":"..."}' | gospect-mcp propose-fixgospect-mcp fix closes the loop: it drives a system AI agent to apply the fix from that
envelope, then verifies the result and rolls back on any regression. gospect stays the sensor
and the verifier — the agent only actuates.
gospect-mcp fix -detector nilness . # auto-detect an installed agent, fix one nilness finding
gospect-mcp fix -min-severity high -n 5 . # fix up to 5 findings (each verified fix is committed)
gospect-mcp fix -safe -staticcheck . # deterministic analyzer fixes only — no AI agent
gospect-mcp fix -agent "aider --yes {prompt}" . # drive a specific agent (or a custom command)
gospect-mcp fix -dry-run -detector nilness . # just print the prompt gospect would sendIt auto-detects claude, aider, cursor-agent, gemini, opencode on your PATH (override
with -agent <name> or a -agent "<command {prompt}>" template). Target findings with the usual
filters (-detector, -min-severity, -category); -n fixes several in one run (committing each
verified fix so the next starts clean — git reset --soft HEAD~N to uncommit them).
-safe (no AI). Some analyzer findings ship a mechanical fix; -safe applies those directly —
no agent involved — still through the full verify harness. It is deliberately conservative: it
applies a fix only when the analyzer offers exactly one (an ambiguous choice, like !!b → !b
or b, is left to you or an agent). Findings without a single clear fix are skipped.
The safety contract — a fix is kept only if all of these hold; otherwise the working tree is restored exactly:
- Requires a clean git tree to start (so any change is cleanly reversible).
- After the agent edits, gospect re-scans: the target finding must be gone, and no new findings may appear.
- The module must still build (
go build ./...) — add-testto also requirego test. - On success the change is left uncommitted for you to review (
git diff); nothing is committed.
The agent's own output streams to stderr as it works, so a multi-minute fix visibly makes
progress (silence it with -quiet). Each fix has a time budget — -timeout (default 5m, 0
disables) — after which the agent is killed and that finding is rolled back and skipped.
Exit codes: 0 fixed, 1 not applied (rolled back), 2 error (e.g. dirty tree, no agent found).
By default the MCP server is a pure sensor — it exposes only scan and propose_fix and never
edits code. If you want an MCP host to request a verified fix, start the server with --allow-fix
(or GOSPECT_ALLOW_FIX=1, handy for host config env):
That registers one extra tool, fix, which is deliberately conservative:
- Deterministic only — it applies the analyzer's mechanical fix (the
-safepath). The server has no AI model; it never drives an agent. Findings without exactly one unambiguous fix come backapplied: false. - Self-verifying — same safety contract as the CLI: clean git tree required, re-scan (target
gone + no new findings),
go buildmust pass (test: truealso runsgo test), else the working tree is rolled back. - Off unless you ask — no flag, no tool. The default server can't touch your code.
Call it with the finding fields from scan (detector, file, optional line) plus the module
path; it returns the same Result JSON the CLI produces (with the diff on success).
Go module ──► go/packages (load + type-check) ──► detectors ──► Report (JSON)
the risky core bug / missing / modernize
- Load the target packages with full types + syntax via
go/packages. - Detect — run curated
go/analysispasses plus lightweight AST checks. Each diagnostic becomes aFinding. - Report — aggregate, de-dupe, sort, summarize. Never edit.
The MCP layer is a hand-rolled JSON-RPC 2.0 stdio server (initialize, tools/list,
tools/call) — no SDK dependency.
Pick whichever fits — all give you the same gospect-mcp binary.
One-line install (macOS / Linux) — downloads the latest release binary for your platform:
curl -fsSL https://raw.githubusercontent.com/backendArchitect/gospect-mcp/main/install.sh | bashOverride the target dir with GOSPECT_INSTALL_DIR or the version with GOSPECT_VERSION.
Windows (PowerShell) — installs to %LOCALAPPDATA%\gospect-mcp and adds it to your PATH:
irm https://raw.githubusercontent.com/backendArchitect/gospect-mcp/main/install.ps1 | iexWith Go (any platform with a Go toolchain):
go install github.com/backendArchitect/gospect-mcp@latestPrebuilt binaries — Linux / macOS / Windows, amd64 & arm64 — from the
Releases page. Each is a
gospect-mcp_<tag>_<os>_<arch>.tar.gz with a .sha256 checksum.
From source:
git clone git@github.com:backendArchitect/gospect-mcp.git
cd gospect-mcp
go build -o gospect-mcp . # build the binary
go test ./... # run the tests
./gospect-mcp scan ./testdata/buggytestdata/buggy is a separate module with deliberate issues (a nil-deref, a stub, a TODO, an
unchecked error, an old go.mod) that the test suite asserts each detector catches.
- PRs run
go vet+go test+go build(ci.yml). - Pushes to
mainrun the tests and then auto-cut a release (release.yml): patch-bump avX.Y.Ztag and publish cross-platform binaries + checksums to GitHub Releases. Add[skip release]to a commit message to skip releasing.
Contributions are welcome! See CONTRIBUTING.md for how to build, test, and get a change merged, and please read the Code of Conduct.