feat(pganalyze): migrate to official MCP server proxy#156
Conversation
Replace the custom GraphQL API client (3 tools) with an HTTP-based MCP proxy that connects to pganalyze's new MCP server at https://app.pganalyze.com/mcp. This expands coverage from 3 to 20 tools including tables, EXPLAIN plans, backends, index advisor, and more. Auth is now Bearer token via API key (matching pganalyze's MCP spec). The organization_slug credential is no longer needed. 🐘 Generated with Crush Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
acmacalister
left a comment
There was a problem hiding this comment.
Nice refactor — the proxy architecture is much cleaner than the bespoke GraphQL client, and the test coverage is thorough. Two things worth fixing before merge: a confirmed data race on lazy proxy init, and an unbounded response read.
| } | ||
| _, err := p.gql(ctx, `{ __typename }`, nil) | ||
| return err == nil | ||
| if !p.started { |
There was a problem hiding this comment.
started and proxy are read here and in Execute while startProxy writes them with no synchronization — I confirmed this triggers the race detector. Two goroutines hitting Healthy and Execute concurrently (normal for an MCP server handling parallel tool calls) will race on both fields.
sync.Once is the idiomatic fix:
type pganalyze struct {
apiKey string
mcpURL string
client *http.Client
once sync.Once
proxy *proxyClient // written once via once.Do, safe to read after
}
func (p *pganalyze) ensureProxy(ctx context.Context) {
p.once.Do(func() {
proxy := newProxyClient(p.mcpURL, p.apiKey, p.client)
if err := proxy.initialize(ctx); err != nil {
return
}
if err := proxy.fetchTools(ctx); err != nil {
return
}
p.proxy = proxy
})
}Then Healthy and Execute both call p.ensureProxy(ctx) instead of checking p.started. The Once guarantees proxy is fully written before any reader sees it.
| } | ||
| defer func() { _ = resp.Body.Close() }() | ||
|
|
||
| respBody, err := io.ReadAll(resp.Body) |
There was a problem hiding this comment.
Every other HTTP adapter in the codebase caps response reads with io.LimitReader (snowflake, nomad, jira, fly, etc. all use a maxResponseSize constant). This one doesn't, so a large or malicious response from the MCP endpoint — especially relevant since base_url is user-configurable — could cause unbounded memory growth.
const maxResponseSize = 10 * 1024 * 1024 // 10 MB
// in send():
respBody, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
Summary
https://app.pganalyze.com/mcp)organization_slugcredential removedNew Tools (17 added)
get_server_details,get_postgres_settingsget_databasesget_query_details,get_query_samplesget_tables,get_table,get_table_stats,get_index_selection,run_index_selectionget_query_explains,get_query_explain,get_query_explain_from_traceget_backend_counts,get_backends,get_backend_detailsget_checkup_statusTest plan
make cipasses (build, vet, test-race, lint, security)organization_slug🐘 Generated with Crush