Skip to content

feat(pganalyze): migrate to official MCP server proxy#156

Merged
aleksclark merged 1 commit into
mainfrom
aleks/update-to-pganalyze-mcp
May 7, 2026
Merged

feat(pganalyze): migrate to official MCP server proxy#156
aleksclark merged 1 commit into
mainfrom
aleks/update-to-pganalyze-mcp

Conversation

@aleksclark
Copy link
Copy Markdown
Collaborator

Summary

  • Replaces the custom GraphQL API client (3 tools) with an HTTP-based MCP proxy to pganalyze's official MCP server (https://app.pganalyze.com/mcp)
  • Expands from 3 tools to 20 tools: adds tables, EXPLAIN plans, backends, index advisor, postgres settings, query details/samples, and more
  • Auth simplified to Bearer token (API key) — organization_slug credential removed

New Tools (17 added)

Category Tools
Servers get_server_details, get_postgres_settings
Databases get_databases
Queries get_query_details, get_query_samples
Tables get_tables, get_table, get_table_stats, get_index_selection, run_index_selection
EXPLAIN Plans get_query_explains, get_query_explain, get_query_explain_from_trace
Backends get_backend_counts, get_backends, get_backend_details
Issues get_checkup_status

Test plan

  • All 27 pganalyze unit tests pass (proxy lifecycle, tool execution, error handling, rate limiting)
  • Full make ci passes (build, vet, test-race, lint, security)
  • Search benchmark still indexes pganalyze tools correctly (static fallback)
  • Config tests updated for removed organization_slug

🐘 Generated with Crush

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>
@aleksclark aleksclark merged commit 211b791 into main May 7, 2026
5 checks passed
@aleksclark aleksclark deleted the aleks/update-to-pganalyze-mcp branch May 7, 2026 17:31
Copy link
Copy Markdown
Collaborator

@acmacalister acmacalister left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

@pagkt2 pagkt2 mentioned this pull request May 22, 2026
3 tasks
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.

2 participants