GoVisual and the case for giving a coding agent eyes into your Go app
Zero-config, pure-Go HTTP request visualizer & debugger for local Go web development.
At a glance
- What is it?
- GoVisual is a zero-config HTTP debugger for Go that wraps your handler, captures every request with its SQL, outbound calls, logs and panics attached, and exposes the result to a dashboard and to an MCP server. Its design bets that the interesting part of debugging an agent-written service is replaying a real request rather than reading new code.
- Who is it for?
- GoVisual suits a Go team that wants request-level visibility without standing up a tracing backend, and it suits a team working with a coding agent, because the MCP server lets the agent list, replay and diff real requests instead of guessing. It does not suit a production service, since the dashboard is a local debugging surface and the storage is bounded in memory by default.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 51 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Wrap the handler, get a dashboard
GoVisual is a runtime HTTP debugger for Go, and the integration is one function call around your existing mux.
package main
import (
"net/http"
"github.com/doganarif/govisual/v2"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/users", userHandler)
handler := govisual.Wrap(mux,
govisual.WithRequestBodyLogging(true),
govisual.WithResponseBodyLogging(true),
)
http.ListenAndServe(":8080", handler)
}That is the whole setup for the basic case. The handler is wrapped, the body logging flags turn request and response bodies on, and the dashboard appears at http://localhost:8080/__viz. The path is configurable with WithDashboardPath, so it can be moved to something like /__debug if that suits your routes better.
The important property is that this is a library rather than a proxy or a sidecar. Your app still listens on its own port, there is no second process to run and no configuration file, which is why the project calls it zero-config and pure-Go.
The module is github.com/doganarif/govisual/v2 and the go.mod requires one dependency, google/pprof, with Go 1.24.0 as the floor. The core deliberately has no database drivers, no gRPC and no OpenTelemetry SDK, so a project that only wants the dashboard does not inherit any of that.
The primary language is Go, the licence is MIT, and the repository is not archived.
The MCP server is the part that changes how you work with an agent
The 2.0 release added an MCP server, and it is the reason the project is worth an article rather than a mention.
The mcp module serves captured traffic to any MCP client, and the description of the feature list is precise about what an agent can do: read the most recent error, pull the full crime scene for one request, replay it after you change the code, and diff the response. There are twelve tools in total, named in the guide: get_last_error, list_requests, get_request, search_requests, get_stats, get_debug_context, replay_request, diff_replay, await_request, save_as_test, copy_as_curl, clear_requests.
The setup mirrors the wrapping pattern, with a store and two muxes:
package main
import (
"net/http"
gvmcp "github.com/doganarif/govisual/mcp"
"github.com/doganarif/govisual/v2"
"github.com/doganarif/govisual/v2/store"
)
func main() {
mux := http.NewServeMux()
// ...routes...
st := store.WithNotify(store.NewMemory(200))
app := govisual.Wrap(mux, govisual.WithStore(st))
root := http.NewServeMux()
root.Handle("/mcp", gvmcp.Handler(st, gvmcp.WithBaseURL("http://localhost:8080")))
root.Handle("/", app)
http.ListenAndServe(":8080", root)
}Registering it with Claude Code is one command:
claude mcp add govisual --transport http http://localhost:8080/mcpThe security model here is worth reading carefully. The endpoint is loopback-only by default, and the dashboard is loopback-only by default as well, with replay and system-info endpoints opt-in. Replay, diff and the generated curl commands require and are pinned to WithBaseURL, which means an agent cannot be talked into pointing replay at an arbitrary host by a request body.
The responses are described as token-aware, with bounded lists and diagnostics, capped body excerpts, and sizes reported so the agent knows when to ask for more. That is a small design decision with a large effect on an agent loop, because an unbounded dump of captured traffic is a context blowout.
Everything is attached to the request that caused it
The per-request capture is the core idea, and it works by wrapping the things that already exist in a Go service.
SQL is captured by wrapping the database driver. Queries executed with the request's context land on that request's profile with duration, affected rows and any error:
import (
"database/sql"
"github.com/doganarif/govisual/v2"
_ "github.com/mattn/go-sqlite3" // your driver of choice
)
sql.Register("sqlite3+viz", govisual.WrapDriver(&sqlite3.SQLiteDriver{}))
db, err := sql.Open("sqlite3+viz", "app.db")
// then in a handler:
rows, err := db.QueryContext(r.Context(), "SELECT ...")The rule is the same everywhere: pass the request context, and the activity is attributed to that request. Enable govisual.WithProfiling(true) on the wrap call and the SQL panel shows up on the request drawer's Trace tab.
Outbound HTTP works the same way, by wrapping the http.RoundTripper:
client := &http.Client{Transport: govisual.WrapTransport(nil)}
resp, err := client.Do(req.WithContext(r.Context()))Application logs go through the slog handler:
logger := slog.New(govisual.SlogHandler(slog.NewJSONHandler(os.Stdout, nil)))
// in a handler:
logger.InfoContext(r.Context(), "cache miss", "key", key)And your base handler still receives every record unchanged, which is the part that makes it safe to leave in place while you work.
You can also push your own events:
govisual.Event(r.Context(), "cache miss", "key", key, "tier", "redis")which appears on the request's Logs tab and, with profiling on, inside the middleware trace.
Panics are handled in a way that matters: handler panics land on the captured request with the panic value and full stack, then the panic is re-raised so recovery middleware and net/http behave exactly as they would without govisual in the chain. That re-raise is the difference between a debugger you can leave in and one you cannot.
The dashboard is a request drawer, not a chart of everything
The dashboard layout is organised around triage rather than around monitoring, and the difference shows in the panels.
There are three lists, Inbox, Errors and Slow, for filtering captured requests by status and duration. Each request opens a drawer with tabs: Overview, Headers, Body, Trace for middleware, SQL and outbound HTTP, Logs, and Performance, which covers allocations, process heap, goroutines, GC and a flame graph when profiling is on.
The Analytics section gives per-route request counts, p50 and p95, and error rates. The Agents section shows recent MCP tool calls with their arguments, so you can watch a coding agent debug the app in real time. The Environment panel reports the Go version, GOOS and GOARCH, memory stats and allowlisted environment variables.
There is dark mode, toggled from the rail, with the choice persisted via localStorage, and push updates over SSE, so new requests appear as they land rather than on a poll.
The Environment panel is worth a second look, because allowlisted environment variables is a deliberate restriction. A debugging tool that dumps the whole environment is a credential leak waiting to happen, so the tool asks you to say which variables are safe to show.
The performance panel is the one place where the pprof dependency earns its place. Allocations, heap, goroutine and GC figures per request, with a flame graph, is the detail you need when the bug is a memory profile rather than a wrong value.
Storage is in-memory by default, and the add-ons are optional modules
The 2.0 restructure made the dependency graph explicit, and the top-level tree reflects it: a separate store/ directory, a separate mcp/ directory, a separate telemetry/ directory, and the core files at the root, wrap.go, options.go, event.go, slog.go, sql.go, transport.go and govisual.go.
By default, requests live in memory, bounded by WithMaxRequests, which is 100. For persistence you install one of the storage modules and pass its constructor to WithStore. The postgres one looks like this:
import (
"github.com/doganarif/govisual/store/postgres"
"github.com/doganarif/govisual/v2"
)
st, err := postgres.New(
"postgres://user:pass@localhost:5432/dbname?sslmode=disable",
"govisual_requests", // table, created and migrated automatically
500, // capacity
)
if err != nil {
log.Fatal(err)
}
handler := govisual.Wrap(mux, govisual.WithStore(st))The other backends, redis, sqlite and mongodb, take the same shape. The table is created and migrated automatically, so there is no schema to manage.
The telemetry module is separate again, for OpenTelemetry export, and the install commands show the three optional modules as distinct go get lines:
go get github.com/doganarif/govisual/mcp # MCP server for coding agents
go get github.com/doganarif/govisual/store/postgres # or redis, sqlite, mongodb
go get github.com/doganarif/govisual/telemetry # OpenTelemetry exportThe memory store in the MCP example, store.NewMemory(200), is wrapped in store.WithNotify, which is how the dashboard gets pushed new requests over SSE as they land.
One migration note: the guide flags an upgrade path from v0.2.1, and the release history shows the jump, with v0.2.1 from 2026-05-22, v2.0.0 from 2026-07-02 and v2.0.1 from 2026-09-03. The import path also carries the major version, so the code you find in the guide is for the v2 line.
Where GoVisual is the wrong tool
The first case is a production deployment. This is a debugging surface, not an observability system. The default storage is a bounded in-memory buffer, the dashboard is served from your application, and the whole point of the loopback restriction is that it should not be exposed. If you want request telemetry in production, that is OpenTelemetry, and this project ships a telemetry module for exporting rather than for replacing it.
The second is a project that already has a tracing stack. If you already run OpenTelemetry or a logging pipeline, wrapping the same handler again to get a second view of it is redundant work, and you would end up maintaining two request models.
The third is a codebase where the context is not threaded. The whole capture mechanism depends on the request context being passed to the database call, the outbound HTTP call and the logger. A handler that builds its own context or calls a background goroutine without propagating the request context will produce a dashboard with gaps, and the gaps look like missing bugs rather than missing instrumentation. That is a real adoption cost in a codebase that predates context propagation habits.
The fourth is a team that does not want a coding agent with replay access. The MCP server gives an agent the ability to reissue requests against the running application, with a base URL it is pinned to. That is a significant debugging capability, and it is also a capability you have decided to hand to a model. Whether that is acceptable is a decision about your threat model rather than about the tool.
Editorial conclusion
GoVisual suits a Go team that wants request-level visibility without standing up a tracing backend, and it suits a team working with a coding agent, because the MCP server lets the agent list, replay and diff real requests instead of guessing. It does not suit a production service, since the dashboard is a local debugging surface and the storage is bounded in memory by default. Before you adopt it, read the loopback note, because the dashboard and the replay endpoints are local-only unless you deliberately open them, and note that the core module has one dependency, google/pprof, and that the last push was on 2026-09-03 with v2.0.1 published the same day.
Frequently asked questions
How do I add GoVisual to a Go service?
Wrap your handler with govisual.Wrap, optionally with WithRequestBodyLogging and WithResponseBodyLogging, and serve the result. The dashboard then appears at http://localhost:8080/__viz by default, and the path is changeable with WithDashboardPath.
What can a coding agent do through the GoVisual MCP server?
Twelve tools are exposed, including get_last_error, list_requests, get_request, search_requests, get_stats, get_debug_context, replay_request, diff_replay, await_request, save_as_test, copy_as_curl and clear_requests. The endpoint is loopback-only by default, and replay, diff and curl generation are pinned to the configured WithBaseURL.
How does GoVisual capture SQL queries and outbound HTTP calls?
By wrapping the database driver with WrapDriver and the http.RoundTripper with WrapTransport. Activity executed with the request's context is attributed to that request, with duration, affected rows and errors for SQL. Enable WithProfiling(true) for the SQL panel to appear on the Trace tab.
What dependencies does GoVisual pull in?
The core module has one dependency, google/pprof, and no database drivers, no gRPC and no OpenTelemetry SDK. Storage backends for postgres, redis, sqlite and mongodb, the MCP server and the telemetry export are separate modules you install only when you need them.
How do I keep captured requests across restarts in GoVisual?
By default requests are held in memory, bounded by WithMaxRequests which is 100. For persistence, install a storage module and pass its constructor to WithStore, for example postgres.New with a connection string, a table name that is created and migrated automatically, and a capacity.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/doganarif-govisual)Was this useful?