Repository navigation
fix(mcp): harden gosqlx-mcp HTTP server (rate limiting, timeouts, auth, tool errors) - #831
Conversation
Embed an analyze_sql report widget (ui://gosqlx/analysis-v1.html), return structuredContent from all tools with unchanged text output, add server instructions, noauth securitySchemes and invocation status metadata, accept mariadb/clickhouse in validate_sql, report gosqlx.Version and the real tool count, serve the optional OpenAI apps domain challenge, and sweep idle MCP sessions after 30 minutes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L2wnDjnjXs2u8baaf2PC6w
Install with /plugin marketplace add ajitpratap0/GoSQLX and /plugin install gosqlx@gosqlx: hosted MCP server, five SQL skills, four slash commands, a read-only sql-reviewer agent and an opt-out .sql validation hook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L2wnDjnjXs2u8baaf2PC6w
Add gosqlx.dev/terms, describe hosted MCP server, ChatGPT app and Claude plugin data handling accurately in the privacy policy, document both integrations in README, MCP guide and CHANGELOG, and fix the Claude Desktop stdio example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L2wnDjnjXs2u8baaf2PC6w
Name the maintainer as controller and contracting party, add a private contact and grievance officer, legal bases, recipients (incl. Vercel), international transfers, retention, security, user rights, children, DPDP Act 2023 and GDPR references; terms gain eligibility, liability cap, consumer carve-out, indemnity, termination, Indian governing law, boilerplate and conspicuous disclaimers. Correct the VS Code telemetry opt-in text and replace its dead PRIVACY.md link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L2wnDjnjXs2u8baaf2PC6w
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L2wnDjnjXs2u8baaf2PC6w
…t_sql - widget: cap long lists (counted statement types, +N more), show failed tool calls as errors on both host bridges, skip clipboard API when the frame's permissions policy blocks it - server: one streamable transport per Server reused by Handler/Start, new Server.Shutdown, Start gains read-header/idle timeouts, transport logger drops Info so session IDs are never logged - format_sql: uppercase_keywords=true also uppercases operator/function keywords; option semantics documented precisely - docs/privacy: session state is swept 30-45 minutes after last request and IDs are not tracked - tests: full HTTP chain via newMux, session TTL wiring, sweeper leak, widget script structure, constants and metadata keys Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L2wnDjnjXs2u8baaf2PC6w
…rrors - rate limit keys on RemoteAddr; X-Forwarded-For no longer trusted by default; opt-in GOSQLX_MCP_TRUSTED_PROXY_HEADER / _HOPS with IP validation (render.yaml: CF-Connecting-IP) - bounded limiter memory with per-shard LRU eviction - GET/SSE streams excluded from load metric, capped at 4 per IP - POST body read deadline (408), idle timeout, 32KB header cap, read deadline on non-/mcp routes so stalled bodies cannot hold connections - oversized bodies get 413; denials are HTTP 429 with Retry-After and the request id (initialize keeps 200 for mcp-go clients); batches charged per tool call - bearer auth: constant-time digest compare, case-insensitive scheme - tool input problems return isError results naming the argument; indent_size limited to 0-16 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L2wnDjnjXs2u8baaf2PC6w
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @ajitpratap0's task in 32s —— View job Review of PR #831: gosqlx-mcp HTTP hardeningI read Overall: The change is well structured and the rate limiter is sound. The per-shard LRU bounds memory, Issues and suggestions
Nits
VerdictThere are no blocking bugs in the parts I reviewed. Items 1, 2 and 4 are worth addressing before merge, and the rest can follow. Please complete the unchecked post-deploy item (confirm Render forwards |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9
|
Retargeted to main after #823 squash-merged. Resolved conflicts by merging origin/main (e80fef3): the branch still carried #823's pre-squash commits, so for the 10 conflicted files I took main's version and re-applied only this PR's own diff (48bda6e..3eebd5d). Net diff vs main is this PR's 15 files only. gofmt, vet, golangci-lint and go test -race on pkg/mcp and cmd/gosqlx-mcp pass. 🤖 Generated with Claude Code |
|
Claude finished @ajitpratap0's task in 28s —— View job Review of PR #831: gosqlx-mcp HTTP hardeningI read the request path in Overall: The design is sound. The default of keying on Issues
Nits
VerdictI found no blocking bugs. Please address items 1, 3 and 7 before merge, and complete the unchecked post-deploy item (confirm Render forwards |
#831 was squash-merged; took main's versions of its files and re-applied only this PR's pkg/mcp/server.go change (import block union). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9
- Reserve a GET stream slot before charging the rate limit, so a GET refused by the 4-streams-per-client cap costs no budget; a GET denied by the limiter releases its slot. - Log one startup warning when no trusted proxy is configured: behind a proxy every client shares one bucket and stream cap. - Count body read-deadline errors and log the first with a fixed message (error type only; no session IDs, bodies or addresses). - Add opt-in GOSQLX_MCP_TRUSTED_PROXY_SECRET_HEADER and GOSQLX_MCP_TRUSTED_PROXY_SECRET: forwarded client IPs are trusted only when the header matches (SHA-256 + constant-time compare), otherwise RemoteAddr is used. The header is stripped before the MCP handler. Default behaviour is unchanged. - Non-empty unparseable POST bodies and empty batches cost 1 unit; a batched initialize gets HTTP 200 on denial like a single initialize. - Document that Server.Start/Handler skip RateLimitMiddleware, the Cloudflare Transform Rule setup, and the shared limit for ChatGPT. Closes #838 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 Co-authored-by: Ajit Pratap Singh <ajitpratapsingh@Ajits-Mac-mini-2655.local> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Hardens the
gosqlx-mcpHTTP server against the pre-existing security/robustness defects found by the PR #823 test run (all also present onmain). Stacked on #823 — review/merge #823 first; this PR then retargets tomain.Description
X-Forwarded-ForRemoteAddrby default; opt-inGOSQLX_MCP_TRUSTED_PROXY_HEADER/GOSQLX_MCP_TRUSTED_PROXY_HOPS, values validated withnet.ParseIP, IPv6 grouped by /64;render.yamlsetsCF-Connecting-IP/mcp),IdleTimeout120 s,MaxHeaderBytes32 KB; no global Read/WriteTimeout so SSE streams surviveanalyze_sql)crypto/subtle;Bearercase-insensitive, token trimmedid: null, noRetry-AfterRetry-After+ request id; accurateX-RateLimit-*; batches charged per tool (deniedinitializestays 200 because mcp-go treats any 4xx there as a legacy SSE server)isErrortool results naming the argument;indent_sizemust be an integer 0–16Type of Change
Test Plan
task check(fmt, vet, golangci-lint 0 issues, race tests) passes/mcpslow-body deadline (with a no-fix control case), bearer header variants, trusted-proxy config, handler isError cases, HTTP server limitsisErrorwith the same textCF-Connecting-IP(two clients on different networks should see independentX-RateLimit-Remaining); otherwise all users share one bucketBreaking Changes
GOSQLX_MCP_TRUSTED_PROXY_HEADERorGOSQLX_MCP_TRUSTED_PROXY_HOPS, or every client shares the proxy's bucket.isErrorresults instead of JSON-RPC errors;indent_sizeoutside 0–16 or non-integer is rejected.Additional Notes
Residual risk:
CF-Connecting-IPis trusted on any request, so a client that reaches the Render origin URL directly (bypassing Cloudflare) can still choose its rate-limit key. Memory stays bounded. A follow-up could trust the header only when Render's appendedX-Forwarded-Forhop is a Cloudflare IP, or require a secret header set by a Cloudflare Transform Rule.🤖 Generated with Claude Code
https://claude.ai/code/session_01L2wnDjnjXs2u8baaf2PC6w