From 34dff1d4969e8f2c1e513b0141b64225545776fe Mon Sep 17 00:00:00 2001
From: Ajit Pratap Singh
Date: Tue, 20 Oct 2026 00:42:41 +0530
Subject: [PATCH 1/8] feat(mcp): ChatGPT app support via OpenAI Apps SDK and
MCP Apps
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
Claude-Session: https://claude.ai/code/session_01L2wnDjnjXs2u8baaf2PC6w
---
cmd/gosqlx-mcp/main.go | 54 +++-
cmd/gosqlx-mcp/main_test.go | 98 ++++++++
docs/CHATGPT_APP.md | 200 +++++++++++++++
integrations/chatgpt/README.md | 153 +++++++++++
pkg/mcp/apps.go | 154 ++++++++++++
pkg/mcp/apps_test.go | 428 +++++++++++++++++++++++++++++++
pkg/mcp/config.go | 12 +
pkg/mcp/config_test.go | 23 ++
pkg/mcp/server.go | 168 ++++++++-----
pkg/mcp/tools.go | 6 +-
pkg/mcp/widget/analysis.html | 446 +++++++++++++++++++++++++++++++++
render.yaml | 5 +
12 files changed, 1677 insertions(+), 70 deletions(-)
create mode 100644 cmd/gosqlx-mcp/main_test.go
create mode 100644 docs/CHATGPT_APP.md
create mode 100644 integrations/chatgpt/README.md
create mode 100644 pkg/mcp/apps.go
create mode 100644 pkg/mcp/apps_test.go
create mode 100644 pkg/mcp/widget/analysis.html
diff --git a/cmd/gosqlx-mcp/main.go b/cmd/gosqlx-mcp/main.go
index 82d70021..0756875c 100644
--- a/cmd/gosqlx-mcp/main.go
+++ b/cmd/gosqlx-mcp/main.go
@@ -22,6 +22,9 @@
// GOSQLX_MCP_HOST bind host (default: 127.0.0.1)
// GOSQLX_MCP_PORT bind port (default: 8080)
// GOSQLX_MCP_AUTH_TOKEN bearer token; empty disables auth
+// GOSQLX_MCP_OPENAI_APPS_CHALLENGE ChatGPT app domain-verification token
+//
+// Pass --stdio to serve MCP over stdin/stdout instead of HTTP.
//
// # Usage
//
@@ -32,6 +35,7 @@ package main
import (
"context"
+ "encoding/json"
"fmt"
"log"
"net/http"
@@ -88,20 +92,10 @@ func runHTTP() error {
srv := gosqlxmcp.New(cfg)
- // Build handler chain: MCP → auth → rate limiter
- handler := gosqlxmcp.RateLimitMiddleware(srv.Handler())
-
- mux := http.NewServeMux()
- mux.Handle("/mcp", handler)
- mux.Handle("/mcp/", handler)
- mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- fmt.Fprintf(w, `{"status":"ok","version":"%s","tools":7}`, gosqlx.Version)
- })
-
httpSrv := &http.Server{
- Addr: cfg.Addr(),
- Handler: mux,
+ Addr: cfg.Addr(),
+ Handler: newMux(cfg, srv),
+ ReadHeaderTimeout: 10 * time.Second,
}
go func() {
@@ -117,3 +111,37 @@ func runHTTP() error {
}
return nil
}
+
+// newMux builds the HTTP routes:
+//
+// /mcp, /mcp/ MCP streamable HTTP (rate limit → auth → MCP)
+// /health liveness probe with version and tool count
+// /.well-known/openai-apps-challenge ChatGPT app domain verification (only when configured)
+//
+// Every other path (including OAuth discovery probes from connector wizards)
+// returns 404, which tells clients that no OAuth is configured.
+func newMux(cfg *gosqlxmcp.Config, srv *gosqlxmcp.Server) *http.ServeMux {
+ handler := gosqlxmcp.RateLimitMiddleware(srv.Handler())
+
+ mux := http.NewServeMux()
+ mux.Handle("/mcp", handler)
+ mux.Handle("/mcp/", handler)
+
+ health, _ := json.Marshal(map[string]any{
+ "status": "ok",
+ "version": gosqlx.Version,
+ "tools": srv.ToolCount(),
+ })
+ mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write(health)
+ })
+
+ if token := cfg.OpenAIAppsChallenge; token != "" {
+ mux.HandleFunc("/.well-known/openai-apps-challenge", func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ _, _ = w.Write([]byte(token))
+ })
+ }
+ return mux
+}
diff --git a/cmd/gosqlx-mcp/main_test.go b/cmd/gosqlx-mcp/main_test.go
new file mode 100644
index 00000000..6191b406
--- /dev/null
+++ b/cmd/gosqlx-mcp/main_test.go
@@ -0,0 +1,98 @@
+// Copyright 2026 GoSQLX Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
+ gosqlxmcp "github.com/ajitpratap0/GoSQLX/pkg/mcp"
+)
+
+func TestNewMux_Health(t *testing.T) {
+ cfg := gosqlxmcp.DefaultConfig()
+ srv := gosqlxmcp.New(cfg)
+ ts := httptest.NewServer(newMux(cfg, srv))
+ t.Cleanup(ts.Close)
+
+ resp, err := http.Get(ts.URL + "/health")
+ if err != nil {
+ t.Fatalf("GET /health: %v", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ t.Fatalf("status = %d, want 200", resp.StatusCode)
+ }
+ var body map[string]any
+ if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
+ t.Fatalf("decode: %v", err)
+ }
+ if got, ok := body["status"].(string); !ok || got != "ok" {
+ t.Errorf("status = %v, want ok", body["status"])
+ }
+ if got, ok := body["version"].(string); !ok || got != gosqlx.Version {
+ t.Errorf("version = %v, want %q", body["version"], gosqlx.Version)
+ }
+ tools, ok := body["tools"].(float64)
+ if !ok || int(tools) != srv.ToolCount() || srv.ToolCount() == 0 {
+ t.Errorf("tools = %v, want %d", body["tools"], srv.ToolCount())
+ }
+}
+
+func TestNewMux_OpenAIAppsChallenge(t *testing.T) {
+ tests := []struct {
+ name string
+ token string
+ wantStatus int
+ wantBody string
+ }{
+ {name: "configured", token: "abc123-token", wantStatus: http.StatusOK, wantBody: "abc123-token"},
+ {name: "not configured", token: "", wantStatus: http.StatusNotFound},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cfg := gosqlxmcp.DefaultConfig()
+ cfg.OpenAIAppsChallenge = tt.token
+ ts := httptest.NewServer(newMux(cfg, gosqlxmcp.New(cfg)))
+ t.Cleanup(ts.Close)
+
+ resp, err := http.Get(ts.URL + "/.well-known/openai-apps-challenge")
+ if err != nil {
+ t.Fatalf("GET challenge: %v", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != tt.wantStatus {
+ t.Fatalf("status = %d, want %d", resp.StatusCode, tt.wantStatus)
+ }
+ if tt.wantBody == "" {
+ return
+ }
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatalf("read body: %v", err)
+ }
+ if string(b) != tt.wantBody {
+ t.Errorf("body = %q, want %q", b, tt.wantBody)
+ }
+ if ct := resp.Header.Get("Content-Type"); ct != "text/plain; charset=utf-8" {
+ t.Errorf("Content-Type = %q, want text/plain", ct)
+ }
+ })
+ }
+}
diff --git a/docs/CHATGPT_APP.md b/docs/CHATGPT_APP.md
new file mode 100644
index 00000000..7ebfc1ea
--- /dev/null
+++ b/docs/CHATGPT_APP.md
@@ -0,0 +1,200 @@
+# GoSQLX as a ChatGPT App (Apps SDK / MCP Apps)
+
+`gosqlx-mcp` is one MCP server that works in three kinds of host:
+
+- **Plain MCP clients** (Claude Code, Cursor, MCP Inspector, …). They see seven
+ tools that return JSON text, exactly as before.
+- **MCP Apps hosts** (extension `io.modelcontextprotocol/ui`, spec 2026-01-26, such as
+ Claude). They also render the interactive `analyze_sql` report.
+- **ChatGPT** (OpenAI Apps SDK). It reads the MCP Apps keys and the legacy `openai/*`
+ aliases, and renders the same report.
+
+Hosts ignore `_meta` keys they do not understand. That is why one tool list works
+in all three without negotiating capabilities per host.
+
+User-facing setup is in [integrations/chatgpt/README.md](../integrations/chatgpt/README.md).
+General server documentation is in [MCP_GUIDE.md](MCP_GUIDE.md).
+
+## Source layout
+
+| File | Role |
+|---|---|
+| `pkg/mcp/server.go` | Server construction: version (`gosqlx.Version`), `WithInstructions`, resource capability, tool registration with shared annotations |
+| `pkg/mcp/apps.go` | Widget URI and MIME type, `go:embed` of the widget, tool and resource `_meta` builders, resource registration |
+| `pkg/mcp/widget/analysis.html` | Self-contained report widget (inline CSS/JS, no network) |
+| `pkg/mcp/tools.go` | Tool handlers; `toolResult` returns text plus `structuredContent` |
+| `cmd/gosqlx-mcp/main.go` | HTTP routes: `/mcp`, `/health`, optional `/.well-known/openai-apps-challenge` |
+
+## Initialize
+
+- `serverInfo.version` is `gosqlx.Version`. It is no longer hard-coded.
+- `capabilities` advertises `tools` and `resources`.
+- `instructions` describes when to use each tool. It does not steer the model toward
+ GoSQLX for unrelated requests, which would break the OpenAI submission guidelines.
+
+## Tools
+
+All seven tools (`validate_sql`, `format_sql`, `parse_sql`, `extract_metadata`,
+`security_scan`, `lint_sql`, `analyze_sql`) share these annotations:
+
+```json
+{"title": "...", "readOnlyHint": true, "destructiveHint": false,
+ "idempotentHint": true, "openWorldHint": false}
+```
+
+ChatGPT requires `readOnlyHint`, `destructiveHint` and `openWorldHint` to be set
+explicitly. When they are missing, it assumes the worst case and asks the user to
+confirm every call.
+
+Every tool's `_meta` contains:
+
+| Key | Value | Consumer |
+|---|---|---|
+| `openai/toolInvocation/invoking` | for example `"Validating SQL…"` | ChatGPT status line (≤ 64 chars) |
+| `openai/toolInvocation/invoked` | for example `"Validated SQL"` | ChatGPT status line |
+| `securitySchemes` | `[{"type":"noauth"}]`, only when `GOSQLX_MCP_AUTH_TOKEN` is unset | ChatGPT auth (back-compat `_meta` mirror) |
+
+`analyze_sql` also carries the widget binding:
+
+```json
+"_meta": {
+ "ui": {"resourceUri": "ui://gosqlx/analysis-v1.html", "visibility": ["model", "app"]},
+ "openai/outputTemplate": "ui://gosqlx/analysis-v1.html",
+ "openai/widgetAccessible": true
+}
+```
+
+- `ui.resourceUri` is the MCP Apps source of truth. The deprecated flat key
+ `ui/resourceUri` is not emitted.
+- `openai/outputTemplate` is set to the same URI because some ChatGPT builds only
+ render with the legacy key.
+- `ui.visibility: ["model","app"]` and `openai/widgetAccessible: true` let the widget
+ call the tool again if a later version adds interactivity. The current widget only
+ displays results.
+
+Only `analyze_sql` gets the widget. The widget renders the composite report shape
+(`validate`, `parse`, `metadata`, `security`, `lint`, `format`, `errors`). The other
+tools return different shapes and stay text and JSON only.
+
+The `validate_sql` `dialect` enum is `generic, postgresql, mysql, mariadb, sqlserver,
+oracle, sqlite, snowflake, clickhouse`. All nine are accepted by
+`gosqlx.ParseWithDialect`, and a test checks each one.
+
+`lint_sql` applies the 10 rules that `newFullLinter` registers. They are the same set
+`gosqlx lint` uses: L001–L010, with a line-length limit of 100.
+
+## UI resource
+
+`resources/list`:
+
+```json
+{"uri": "ui://gosqlx/analysis-v1.html", "name": "gosqlx_analysis_report",
+ "mimeType": "text/html;profile=mcp-app", "description": "...", "_meta": { ... }}
+```
+
+`resources/read` returns exactly one `TextResourceContents` whose `text` is the
+embedded HTML. It carries the following `_meta`, which is also copied onto the
+`resources/list` entry:
+
+```json
+{
+ "ui": {
+ "csp": {"connectDomains": [], "resourceDomains": []},
+ "prefersBorder": true,
+ "permissions": {"clipboardWrite": {}}
+ },
+ "openai/widgetCSP": {"connect_domains": [], "resource_domains": []},
+ "openai/widgetPrefersBorder": true,
+ "openai/widgetDescription": "Interactive GoSQLX analysis report: ..."
+}
+```
+
+- **MIME type:** `text/html;profile=mcp-app` (ext-apps `RESOURCE_MIME_TYPE`). ChatGPT
+ accepts it in place of the legacy `text/html+skybridge`.
+- **CSP:** empty on purpose. The widget loads no remote scripts, styles, fonts or
+ images and makes no `fetch`/XHR calls, so the MCP Apps default CSP is enough.
+ Submission still requires a CSP to be declared.
+- **`clipboardWrite`:** requested for the Copy button. When the host does not grant
+ it, the widget falls back to `document.execCommand("copy")`. OpenAI documents that
+ ChatGPT widgets cannot use `navigator.clipboard`, and the sandbox may also block
+ `execCommand`, in which case the button shows "Copy failed" and the user copies
+ from the code block.
+- **Versioned URI:** ChatGPT caches templates by URI. Bump `-v1` in
+ `AnalysisWidgetURI` whenever the widget changes in a way that is incompatible with
+ older results.
+- `openai/widgetDomain` is not set. The widget does not need its own origin.
+
+## Tool results
+
+`toolResult` returns:
+
+```json
+{
+ "content": [{"type": "text", "text": ""}],
+ "structuredContent": { ...same object... }
+}
+```
+
+- The `text` content is byte-for-byte what earlier versions returned, so existing
+ clients and tests keep working. A test checks this.
+- `structuredContent` is the same map. The model sees it, and the widget receives it
+ as `params.structuredContent` in `ui/notifications/tool-result`, or as
+ `window.openai.toolOutput` in ChatGPT.
+- No result-level `_meta` is set. Results contain no request IDs, timestamps or user
+ data (response minimisation).
+- Protocol-level errors (missing `sql`, parse failure in `parse_sql`, …) behave as
+ before.
+
+## Widget runtime
+
+`pkg/mcp/widget/analysis.html` implements a minimal MCP Apps view by hand, without a
+build step or SDK:
+
+1. Posts JSON-RPC `ui/initialize` (with `appInfo`/`clientInfo`, `appCapabilities`,
+ `protocolVersion: "2026-01-26"`) to `window.parent`. It applies
+ `hostContext.theme` and the font variables, then sends
+ `ui/notifications/initialized`.
+2. Handles `ui/notifications/tool-input`, `tool-result`, `tool-cancelled` and
+ `host-context-changed`, and answers `ping` and `ui/resource-teardown`.
+3. Sends `ui/notifications/size-changed` from a `ResizeObserver` on the content
+ root (so the reported height can shrink as well as grow). In ChatGPT it also
+ calls `window.openai.notifyIntrinsicHeight`.
+4. ChatGPT fallback: reads `window.openai.toolOutput`, `toolInput` and `theme`, and
+ listens for `openai:set_globals`.
+5. Theme: follows the host theme (`data-theme`) and otherwise `prefers-color-scheme`.
+
+It renders all data with `textContent`, so SQL text and error messages are never parsed
+as HTML. The test `TestApps_ResourcesRead_ReturnsSelfContainedWidget` fails if the
+HTML gains remote `src`/`href` URLs, `@import`, `fetch(`, `XMLHttpRequest` or
+`innerHTML`.
+
+## HTTP endpoints (`cmd/gosqlx-mcp`)
+
+| Path | Purpose |
+|---|---|
+| `/mcp` | MCP streamable HTTP: rate limit → optional bearer auth → MCP |
+| `/health` | `{"status":"ok","version":,"tools":}` |
+| `/.well-known/openai-apps-challenge` | Plain-text domain-verification token. Served only when `GOSQLX_MCP_OPENAI_APPS_CHALLENGE` is set; returns 404 otherwise |
+
+The server sends no CORS headers. ChatGPT and Claude connect to MCP servers server to
+server, and nothing in the Apps SDK or MCP Apps requirements asks for CORS on the MCP
+endpoint. Browser-based tools such as MCP Inspector go through their own proxy.
+
+## Not implemented (optional)
+
+- `search`/`fetch` tools for ChatGPT company knowledge and deep research. They only
+ make sense over documentation, not over SQL analysis.
+- OAuth. The public server is anonymous and read-only.
+- `openai/widgetDomain` and fullscreen display modes.
+
+## Verifying locally
+
+```bash
+GOSQLX_MCP_PORT=8765 go run ./cmd/gosqlx-mcp &
+H='-H Content-Type:application/json -H Accept:application/json,text/event-stream'
+curl -si $H localhost:8765/mcp -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
+# copy the Mcp-Session-Id response header into SID, then:
+curl -s $H -H "Mcp-Session-Id: $SID" localhost:8765/mcp -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
+curl -s $H -H "Mcp-Session-Id: $SID" localhost:8765/mcp -d '{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"ui://gosqlx/analysis-v1.html"}}'
+curl -s $H -H "Mcp-Session-Id: $SID" localhost:8765/mcp -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"analyze_sql","arguments":{"sql":"SELECT id FROM users WHERE id = 1 OR 1=1"}}}'
+```
diff --git a/integrations/chatgpt/README.md b/integrations/chatgpt/README.md
new file mode 100644
index 00000000..8c85ce93
--- /dev/null
+++ b/integrations/chatgpt/README.md
@@ -0,0 +1,153 @@
+# GoSQLX for ChatGPT
+
+GoSQLX runs in ChatGPT as an app built on the MCP server in
+[`cmd/gosqlx-mcp`](../../cmd/gosqlx-mcp). This is the same server that Claude,
+Cursor and other MCP clients use. ChatGPT gets the same seven read-only SQL tools
+plus an interactive report for `analyze_sql`.
+
+- **Endpoint:** `https://mcp.gosqlx.dev/mcp` (MCP streamable HTTP)
+- **Authentication:** none
+- **What it does:** parses, validates, formats, lints and security-scans SQL text.
+ It never connects to a database and never executes SQL.
+
+Technical details (metadata keys, resource URI, result shapes) are in
+[docs/CHATGPT_APP.md](../../docs/CHATGPT_APP.md).
+
+## Connect in ChatGPT (developer mode)
+
+1. In ChatGPT, open **Settings → Apps & Connectors → Advanced settings** and turn on
+ **Developer mode**. On Business, Enterprise and Edu workspaces an admin has to
+ allow it first.
+2. Back in **Settings → Apps & Connectors**, choose **Create**.
+3. Fill in the form:
+ - **Name:** `GoSQLX`
+ - **Description:** `Parse, validate, format, lint and security-scan SQL across 8 dialects. Read-only; never runs queries.`
+ - **MCP server URL:** `https://mcp.gosqlx.dev/mcp`
+ - **Authentication:** `No authentication`
+4. Tick the trust checkbox and click **Create**. The seven tools are listed.
+5. In a chat, click **+ → More**, pick **GoSQLX**, and ask a question about SQL.
+
+If you change the server (new tools, new descriptions or a new widget), go to
+**Settings → Apps & Connectors → GoSQLX → Refresh** so ChatGPT re-reads the tool list.
+
+Menu labels in ChatGPT change from time to time. If a step above does not match
+what you see, look for "Developer mode" and "Create connector" under Apps or Connectors.
+
+### Other ways to test
+
+- **MCP Inspector:** `npx @modelcontextprotocol/inspector@latest`, choose
+ transport "Streamable HTTP" and URL `https://mcp.gosqlx.dev/mcp`.
+- **OpenAI API Playground:** Tools → Add → MCP Server → the same URL, no auth.
+
+## Example prompts
+
+| Prompt | Tool ChatGPT should pick |
+|---|---|
+| "Review this query: `SELECT * FROM orders o JOIN users u ON o.user_id = u.id WHERE u.email = 'a' OR 1=1`" | `analyze_sql` (renders the report) |
+| "Is `SELECT TOP 10 * FROM t` valid SQL Server syntax?" | `validate_sql` with `dialect: sqlserver` |
+| "Format this SQL with 4-space indentation and uppercase keywords: …" | `format_sql` |
+| "Which tables and columns does this query touch?" | `extract_metadata` |
+| "Does this WHERE clause look like SQL injection? `id = '' OR '1'='1' --`" | `security_scan` |
+| "Lint this SQL for style problems." | `lint_sql` |
+| "How many statements are in this script, and what kinds?" | `parse_sql` |
+
+Prompts where GoSQLX should *not* be used, because it neither runs SQL nor knows your
+schema:
+
+- "How many rows are in my `users` table?"
+- "Write me a migration that adds a column." (GoSQLX can validate the result, but it does not write SQL.)
+- "Connect to my Postgres database."
+
+## What the widget shows
+
+When ChatGPT, or any other host that supports MCP Apps, calls `analyze_sql`, it renders
+an inline report next to the answer:
+
+- a **Valid / Invalid** badge and the parser error, if there is one
+- counts of statements, tables, security findings and lint violations
+- **Statements:** the type of each statement (SELECT, INSERT, …)
+- **Tables, columns & functions** the query references
+- **Security:** injection findings sorted by severity (critical, high, medium, low),
+ each with its risk and a suggested fix
+- **Lint:** violations of rules L001–L010, with line:column and a suggestion
+- **Formatted SQL**, with a **Copy** button
+- **Skipped checks:** sub-checks that could not run (for example, formatting invalid SQL)
+
+The widget is one self-contained HTML file with inline CSS and JS. It makes no network
+requests, follows ChatGPT's light or dark theme, and renders all data as text.
+
+## Self-hosting
+
+The server is one static Go binary.
+
+```bash
+# Docker (repository root)
+docker build -t gosqlx-mcp .
+docker run -p 8080:8080 -e GOSQLX_MCP_HOST=0.0.0.0 gosqlx-mcp
+
+# or from source
+go install github.com/ajitpratap0/GoSQLX/cmd/gosqlx-mcp@latest
+GOSQLX_MCP_HOST=0.0.0.0 gosqlx-mcp
+```
+
+To deploy on Render, use the [`render.yaml`](../../render.yaml) blueprint at the
+repository root. The health check is `GET /health`.
+
+ChatGPT has to reach the server over **public HTTPS**. For local development you can
+use a tunnel such as `ngrok http 8080` or `cloudflared tunnel --url http://localhost:8080`
+and give ChatGPT `https:///mcp`. Tunnels are fine in developer mode but
+are not accepted for directory submission.
+
+| Variable | Purpose |
+|---|---|
+| `GOSQLX_MCP_HOST` / `GOSQLX_MCP_PORT` | Bind address (default `127.0.0.1:8080`). |
+| `GOSQLX_MCP_AUTH_TOKEN` | Optional bearer token. Leave it empty for ChatGPT "No authentication". Once a token is set, tools no longer advertise `securitySchemes: noauth`, and ChatGPT cannot send a static bearer token. |
+| `GOSQLX_MCP_OPENAI_APPS_CHALLENGE` | Domain-verification token from the OpenAI Platform, served as plain text at `/.well-known/openai-apps-challenge`. |
+
+## Privacy
+
+- All tools run locally inside the server process on the SQL text you send. GoSQLX
+ never connects to a database and never executes SQL.
+- The hosted server at `mcp.gosqlx.dev` does not log, store or forward the SQL it
+ processes. Its only state is in memory: MCP session state (swept after 30 minutes of inactivity)
+ and per-IP rate-limit counters (dropped after 10 minutes of inactivity). Render and
+ Cloudflare, which host and front it, see connection metadata such as IP addresses.
+ See .
+- Tool results contain only analysis of the SQL you sent: no user identifiers, request
+ IDs or timestamps. ChatGPT request metadata such as locale or user agent is not used.
+- Do not paste secrets or personal data into SQL you send to *any* hosted service. For
+ sensitive SQL, self-host the server.
+
+## Directory submission checklist
+
+Submit through the OpenAI Platform dashboard (`platform.openai.com/apps-manage`). The
+organization must be verified, and you need the Owner role. Submission is not
+available to projects with EU data residency.
+
+- [ ] Public, stable HTTPS endpoint: `https://mcp.gosqlx.dev/mcp` (not a tunnel).
+- [ ] Domain verification: set `GOSQLX_MCP_OPENAI_APPS_CHALLENGE` in the Render
+ dashboard (`render.yaml` declares it with `sync: false`), redeploy, and confirm `curl https://mcp.gosqlx.dev/.well-known/openai-apps-challenge`
+ returns the token.
+- [ ] CSP declared: the widget resource declares empty `ui.csp` and `openai/widgetCSP`
+ (no remote domains are needed).
+- [ ] Tool annotations and their justifications. Every tool is
+ `readOnlyHint: true`, `destructiveHint: false`, `openWorldHint: false`
+ (`idempotentHint: true`), because each one is a pure function of its input
+ SQL: no writes, no deletes, no network or database access.
+- [ ] Authentication: "No authentication" (`securitySchemes: [{type: "noauth"}]`).
+ No test account is needed.
+- [ ] Privacy policy URL: . It must cover the data
+ categories, purposes, recipients and user controls.
+- [ ] Terms of service URL: .
+- [ ] Support contact: (if the form
+ insists on an email address, use a maintainer-controlled inbox).
+- [ ] Logo (light and dark), at least 3 screenshots of the `analyze_sql` widget, and a
+ short demo video.
+- [ ] Test cases: use the positive and negative example prompts above
+ (reportedly 5 positive and 3 negative).
+- [ ] Review the guidelines: no tool descriptions that push the model to prefer
+ GoSQLX, minimal data collection, no restricted data.
+- [ ] Once the app is published, tool names, signatures and descriptions are locked,
+ and any change needs resubmission. The widget URI carries a version
+ (`ui://gosqlx/analysis-v1.html`); bump it whenever the widget changes in a
+ breaking way.
diff --git a/pkg/mcp/apps.go b/pkg/mcp/apps.go
new file mode 100644
index 00000000..9066fa0d
--- /dev/null
+++ b/pkg/mcp/apps.go
@@ -0,0 +1,154 @@
+// Copyright 2026 GoSQLX Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mcp
+
+import (
+ "context"
+ _ "embed"
+
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+// This file wires the interactive UI ("MCP Apps" / OpenAI Apps SDK) layer.
+//
+// The same metadata serves two host families:
+//
+// - MCP Apps hosts (extension io.modelcontextprotocol/ui, spec 2026-01-26),
+// e.g. Claude: they read _meta.ui.resourceUri on the tool and the
+// _meta.ui.* keys on the resource contents.
+// - ChatGPT (OpenAI Apps SDK): it understands the MCP Apps keys and also
+// the legacy openai/* aliases, which are set to the same values.
+//
+// Hosts that know neither simply ignore the _meta keys, so plain MCP clients
+// (Cursor, Claude Code, MCP Inspector, ...) see no behavioural change.
+
+// AnalysisWidgetURI is the ui:// resource URI of the analyze_sql report widget.
+//
+// The version suffix is part of the URI on purpose: ChatGPT caches templates
+// by URI, so a breaking widget change should bump it (v2, v3, ...).
+const AnalysisWidgetURI = "ui://gosqlx/analysis-v1.html"
+
+// AppMIMEType is the MCP Apps HTML profile MIME type (ext-apps RESOURCE_MIME_TYPE).
+// ChatGPT accepts it as the successor of the legacy "text/html+skybridge".
+const AppMIMEType = "text/html;profile=mcp-app"
+
+// analysisWidgetDescription is shown to the model by ChatGPT alongside the
+// rendered widget so it does not need to re-describe what the user can see.
+const analysisWidgetDescription = "Interactive GoSQLX analysis report: validity, statement types, " +
+ "referenced tables/columns/functions, security findings by severity, lint violations, " +
+ "and the formatted SQL with a copy button."
+
+//go:embed widget/analysis.html
+var analysisWidgetHTML string
+
+// AnalysisWidgetHTML returns the embedded, self-contained HTML of the
+// analyze_sql report widget (inline CSS/JS, no external requests).
+func AnalysisWidgetHTML() string {
+ return analysisWidgetHTML
+}
+
+// widgetResourceMeta is the _meta attached to the widget resource. An empty
+// CSP is declared explicitly: the widget fetches nothing and loads no remote
+// assets, and app submission requires a declared CSP.
+func widgetResourceMeta() map[string]any {
+ return map[string]any{
+ "ui": map[string]any{
+ "csp": map[string]any{
+ "connectDomains": []string{},
+ "resourceDomains": []string{},
+ },
+ "prefersBorder": true,
+ // The "Copy" button uses the async clipboard API when the host
+ // grants it, and falls back to execCommand otherwise.
+ "permissions": map[string]any{
+ "clipboardWrite": map[string]any{},
+ },
+ },
+ "openai/widgetCSP": map[string]any{
+ "connect_domains": []string{},
+ "resource_domains": []string{},
+ },
+ "openai/widgetPrefersBorder": true,
+ "openai/widgetDescription": analysisWidgetDescription,
+ }
+}
+
+// widgetToolMeta returns the tool-descriptor _meta that binds a tool to the
+// analysis widget, in both the MCP Apps form and the OpenAI alias form.
+func widgetToolMeta() map[string]any {
+ return map[string]any{
+ "ui": map[string]any{
+ "resourceUri": AnalysisWidgetURI,
+ "visibility": []string{"model", "app"},
+ },
+ "openai/outputTemplate": AnalysisWidgetURI,
+ "openai/widgetAccessible": true,
+ }
+}
+
+// toolMeta builds the _meta for a tool descriptor.
+//
+// invoking/invoked are ChatGPT status strings (kept well under 64 chars).
+// When withWidget is true the analysis widget is attached. When the server
+// runs without bearer auth the tool is advertised as needing no auth, which
+// is what ChatGPT's connector dialog and app review expect.
+func toolMeta(cfg *Config, invoking, invoked string, withWidget bool) *mcp.Meta {
+ m := map[string]any{
+ "openai/toolInvocation/invoking": invoking,
+ "openai/toolInvocation/invoked": invoked,
+ }
+ if cfg == nil || !cfg.AuthEnabled() {
+ m["securitySchemes"] = []map[string]any{{"type": "noauth"}}
+ }
+ if withWidget {
+ for k, v := range widgetToolMeta() {
+ m[k] = v
+ }
+ }
+ return mcp.NewMetaFromMap(m)
+}
+
+// withToolMeta is a ToolOption that sets the tool's _meta.
+func withToolMeta(meta *mcp.Meta) mcp.ToolOption {
+ return func(t *mcp.Tool) {
+ t.Meta = meta
+ }
+}
+
+// registerResources registers the UI widget resource.
+func (s *Server) registerResources() {
+ res := mcp.NewResource(
+ AnalysisWidgetURI,
+ "gosqlx_analysis_report",
+ mcp.WithResourceDescription(analysisWidgetDescription),
+ mcp.WithMIMEType(AppMIMEType),
+ )
+ res.Meta = mcp.NewMetaFromMap(widgetResourceMeta())
+
+ s.mcpSrv.AddResource(res, handleReadAnalysisWidget)
+}
+
+// handleReadAnalysisWidget serves the widget HTML. resources/read must return
+// exactly one content item for a UI resource.
+func handleReadAnalysisWidget(_ context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
+ return []mcp.ResourceContents{
+ mcp.TextResourceContents{
+ URI: AnalysisWidgetURI,
+ MIMEType: AppMIMEType,
+ Text: analysisWidgetHTML,
+ Meta: widgetResourceMeta(),
+ },
+ }, nil
+}
diff --git a/pkg/mcp/apps_test.go b/pkg/mcp/apps_test.go
new file mode 100644
index 00000000..74bcf6b6
--- /dev/null
+++ b/pkg/mcp/apps_test.go
@@ -0,0 +1,428 @@
+// Copyright 2026 GoSQLX Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mcp
+
+import (
+ "context"
+ "encoding/json"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/mark3labs/mcp-go/client"
+ mcpmcp "github.com/mark3labs/mcp-go/mcp"
+
+ "github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
+)
+
+// newAppsClient starts an in-process client against a server built from cfg
+// and returns it together with the initialize result.
+func newAppsClient(t *testing.T, cfg *Config) (*client.Client, *mcpmcp.InitializeResult) {
+ t.Helper()
+ srv := New(cfg)
+ c, err := client.NewInProcessClient(srv.mcpSrv)
+ if err != nil {
+ t.Fatalf("NewInProcessClient: %v", err)
+ }
+ ctx := context.Background()
+ if err := c.Start(ctx); err != nil {
+ t.Fatalf("client.Start: %v", err)
+ }
+ initReq := mcpmcp.InitializeRequest{}
+ initReq.Params.ProtocolVersion = mcpmcp.LATEST_PROTOCOL_VERSION
+ initReq.Params.ClientInfo = mcpmcp.Implementation{Name: "apps-test", Version: "0.0.1"}
+ res, err := c.Initialize(ctx, initReq)
+ if err != nil {
+ t.Fatalf("client.Initialize: %v", err)
+ }
+ t.Cleanup(func() { _ = c.Close() })
+ return c, res
+}
+
+// metaMap round-trips a value through JSON so nested _meta can be inspected
+// as plain maps regardless of the concrete Go types used to build it.
+func metaMap(t *testing.T, v any) map[string]any {
+ t.Helper()
+ b, err := json.Marshal(v)
+ if err != nil {
+ t.Fatalf("marshal meta: %v", err)
+ }
+ var out map[string]any
+ if err := json.Unmarshal(b, &out); err != nil {
+ t.Fatalf("unmarshal meta: %v", err)
+ }
+ return out
+}
+
+func mustMap(t *testing.T, v any, what string) map[string]any {
+ t.Helper()
+ m, ok := v.(map[string]any)
+ if !ok {
+ t.Fatalf("%s: expected object, got %T (%v)", what, v, v)
+ }
+ return m
+}
+
+func mustEmptyList(t *testing.T, v any, what string) {
+ t.Helper()
+ l, ok := v.([]any)
+ if !ok {
+ t.Fatalf("%s: expected array, got %T (%v)", what, v, v)
+ }
+ if len(l) != 0 {
+ t.Errorf("%s: expected empty array, got %v", what, l)
+ }
+}
+
+// checkWidgetResourceMeta asserts the dual MCP Apps / OpenAI resource metadata.
+func checkWidgetResourceMeta(t *testing.T, meta map[string]any) {
+ t.Helper()
+ ui := mustMap(t, meta["ui"], "_meta.ui")
+ csp := mustMap(t, ui["csp"], "_meta.ui.csp")
+ mustEmptyList(t, csp["connectDomains"], "ui.csp.connectDomains")
+ mustEmptyList(t, csp["resourceDomains"], "ui.csp.resourceDomains")
+ if ui["prefersBorder"] != true {
+ t.Errorf("ui.prefersBorder = %v, want true", ui["prefersBorder"])
+ }
+ perms := mustMap(t, ui["permissions"], "ui.permissions")
+ if _, ok := perms["clipboardWrite"]; !ok {
+ t.Error("ui.permissions.clipboardWrite missing")
+ }
+ oaCSP := mustMap(t, meta["openai/widgetCSP"], "openai/widgetCSP")
+ mustEmptyList(t, oaCSP["connect_domains"], "openai/widgetCSP.connect_domains")
+ mustEmptyList(t, oaCSP["resource_domains"], "openai/widgetCSP.resource_domains")
+ if desc, ok := meta["openai/widgetDescription"].(string); !ok || desc == "" {
+ t.Errorf("openai/widgetDescription missing or empty: %v", meta["openai/widgetDescription"])
+ }
+}
+
+func TestApps_Initialize_InstructionsVersionCapabilities(t *testing.T) {
+ _, res := newAppsClient(t, DefaultConfig())
+
+ if res.ServerInfo.Version != gosqlx.Version {
+ t.Errorf("server version = %q, want gosqlx.Version %q", res.ServerInfo.Version, gosqlx.Version)
+ }
+ if res.Capabilities.Resources == nil {
+ t.Error("resources capability not advertised")
+ }
+ if res.Capabilities.Tools == nil {
+ t.Error("tools capability not advertised")
+ }
+ for _, tool := range []string{"analyze_sql", "validate_sql", "format_sql", "parse_sql", "extract_metadata", "security_scan", "lint_sql"} {
+ if !strings.Contains(res.Instructions, tool) {
+ t.Errorf("instructions do not mention %s", tool)
+ }
+ }
+}
+
+func TestApps_ResourcesList_ExposesWidget(t *testing.T) {
+ c, _ := newAppsClient(t, DefaultConfig())
+
+ res, err := c.ListResources(context.Background(), mcpmcp.ListResourcesRequest{})
+ if err != nil {
+ t.Fatalf("ListResources: %v", err)
+ }
+ if len(res.Resources) != 1 {
+ t.Fatalf("expected 1 resource, got %d", len(res.Resources))
+ }
+ r := res.Resources[0]
+ if r.URI != AnalysisWidgetURI {
+ t.Errorf("URI = %q, want %q", r.URI, AnalysisWidgetURI)
+ }
+ if !strings.HasPrefix(r.URI, "ui://") {
+ t.Errorf("widget URI must use the ui:// scheme, got %q", r.URI)
+ }
+ if r.MIMEType != AppMIMEType {
+ t.Errorf("mimeType = %q, want %q", r.MIMEType, AppMIMEType)
+ }
+ if r.Meta == nil {
+ t.Fatal("resource _meta missing from resources/list")
+ }
+ checkWidgetResourceMeta(t, metaMap(t, r.Meta))
+}
+
+func TestApps_ResourcesRead_ReturnsSelfContainedWidget(t *testing.T) {
+ c, _ := newAppsClient(t, DefaultConfig())
+
+ req := mcpmcp.ReadResourceRequest{}
+ req.Params.URI = AnalysisWidgetURI
+ res, err := c.ReadResource(context.Background(), req)
+ if err != nil {
+ t.Fatalf("ReadResource: %v", err)
+ }
+ if len(res.Contents) != 1 {
+ t.Fatalf("UI resource must return exactly one content item, got %d", len(res.Contents))
+ }
+ tc, ok := res.Contents[0].(mcpmcp.TextResourceContents)
+ if !ok {
+ t.Fatalf("expected TextResourceContents, got %T", res.Contents[0])
+ }
+ if tc.URI != AnalysisWidgetURI {
+ t.Errorf("URI = %q, want %q", tc.URI, AnalysisWidgetURI)
+ }
+ if tc.MIMEType != AppMIMEType {
+ t.Errorf("mimeType = %q, want %q", tc.MIMEType, AppMIMEType)
+ }
+ if tc.Text != AnalysisWidgetHTML() {
+ t.Error("resource text differs from embedded widget HTML")
+ }
+ checkWidgetResourceMeta(t, metaMap(t, tc.Meta))
+
+ lower := strings.ToLower(tc.Text)
+ if !strings.HasPrefix(lower, "") {
+ t.Error("widget must be an HTML5 document")
+ }
+ // Self-contained: the declared CSP has no domains, so nothing remote may load.
+ for _, forbidden := range []string{
+ `src="http`, `src='http`, `href="http`, `href='http`, "@import", "fetch(", "xmlhttprequest", "innerhtml",
+ } {
+ if strings.Contains(lower, forbidden) {
+ t.Errorf("widget HTML contains %q; it must be self-contained and render via textContent", forbidden)
+ }
+ }
+ // The widget must speak the MCP Apps bridge.
+ for _, want := range []string{"ui/initialize", "ui/notifications/initialized", "ui/notifications/tool-result", "structuredContent"} {
+ if !strings.Contains(tc.Text, want) {
+ t.Errorf("widget HTML does not reference %q", want)
+ }
+ }
+}
+
+func TestApps_ToolsList_MetaAndAnnotations(t *testing.T) {
+ tests := []struct {
+ name string
+ authToken string
+ wantNoAuth bool
+ }{
+ {name: "no auth advertises noauth security scheme", authToken: "", wantNoAuth: true},
+ {name: "bearer auth omits noauth security scheme", authToken: "secret", wantNoAuth: false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cfg := DefaultConfig()
+ cfg.AuthToken = tt.authToken
+ c, _ := newAppsClient(t, cfg)
+
+ res, err := c.ListTools(context.Background(), mcpmcp.ListToolsRequest{})
+ if err != nil {
+ t.Fatalf("ListTools: %v", err)
+ }
+ if len(res.Tools) != 7 {
+ t.Fatalf("expected 7 tools, got %d", len(res.Tools))
+ }
+ for _, tool := range res.Tools {
+ a := tool.Annotations
+ if a.ReadOnlyHint == nil || !*a.ReadOnlyHint {
+ t.Errorf("%s: readOnlyHint must be true", tool.Name)
+ }
+ if a.DestructiveHint == nil || *a.DestructiveHint {
+ t.Errorf("%s: destructiveHint must be false", tool.Name)
+ }
+ if a.OpenWorldHint == nil || *a.OpenWorldHint {
+ t.Errorf("%s: openWorldHint must be false", tool.Name)
+ }
+ if a.Title == "" {
+ t.Errorf("%s: title annotation missing", tool.Name)
+ }
+ if tool.Meta == nil {
+ t.Fatalf("%s: _meta missing", tool.Name)
+ }
+ meta := metaMap(t, tool.Meta)
+
+ for _, k := range []string{"openai/toolInvocation/invoking", "openai/toolInvocation/invoked"} {
+ s, ok := meta[k].(string)
+ if !ok || s == "" || len([]rune(s)) > 64 {
+ t.Errorf("%s: %s = %v, want non-empty string <= 64 chars", tool.Name, k, meta[k])
+ }
+ }
+
+ schemes, hasSchemes := meta["securitySchemes"]
+ if tt.wantNoAuth {
+ list, ok := schemes.([]any)
+ if !ok || len(list) != 1 {
+ t.Fatalf("%s: securitySchemes = %v, want one noauth entry", tool.Name, schemes)
+ }
+ if mustMap(t, list[0], "securitySchemes[0]")["type"] != "noauth" {
+ t.Errorf("%s: securitySchemes[0].type != noauth", tool.Name)
+ }
+ } else if hasSchemes {
+ t.Errorf("%s: securitySchemes must be omitted when bearer auth is enabled", tool.Name)
+ }
+
+ _, hasTemplate := meta["openai/outputTemplate"]
+ _, hasUI := meta["ui"]
+ if tool.Name != "analyze_sql" {
+ if hasTemplate || hasUI {
+ t.Errorf("%s: only analyze_sql should be bound to the widget", tool.Name)
+ }
+ continue
+ }
+ ui := mustMap(t, meta["ui"], "analyze_sql _meta.ui")
+ if ui["resourceUri"] != AnalysisWidgetURI {
+ t.Errorf("analyze_sql ui.resourceUri = %v, want %q", ui["resourceUri"], AnalysisWidgetURI)
+ }
+ if meta["openai/outputTemplate"] != AnalysisWidgetURI {
+ t.Errorf("analyze_sql openai/outputTemplate = %v, want %q", meta["openai/outputTemplate"], AnalysisWidgetURI)
+ }
+ if meta["openai/widgetAccessible"] != true {
+ t.Errorf("analyze_sql openai/widgetAccessible = %v, want true", meta["openai/widgetAccessible"])
+ }
+ if _, deprecated := meta["ui/resourceUri"]; deprecated {
+ t.Error("analyze_sql uses deprecated flat ui/resourceUri key")
+ }
+ }
+ })
+ }
+}
+
+func TestApps_ToolsList_ValidateDialectEnum(t *testing.T) {
+ c, _ := newAppsClient(t, DefaultConfig())
+ res, err := c.ListTools(context.Background(), mcpmcp.ListToolsRequest{})
+ if err != nil {
+ t.Fatalf("ListTools: %v", err)
+ }
+ for _, tool := range res.Tools {
+ if tool.Name != "validate_sql" {
+ continue
+ }
+ dialect := mustMap(t, tool.InputSchema.Properties["dialect"], "dialect schema")
+ enum, ok := dialect["enum"].([]any)
+ if !ok {
+ b, _ := json.Marshal(dialect["enum"])
+ if err := json.Unmarshal(b, &enum); err != nil {
+ t.Fatalf("dialect enum has unexpected type %T", dialect["enum"])
+ }
+ }
+ got := map[string]bool{}
+ for _, v := range enum {
+ s, ok := v.(string)
+ if !ok {
+ t.Fatalf("enum value %v is %T, want string", v, v)
+ }
+ got[s] = true
+ }
+ for _, want := range []string{"generic", "postgresql", "mysql", "mariadb", "sqlserver", "oracle", "sqlite", "snowflake", "clickhouse"} {
+ if !got[want] {
+ t.Errorf("dialect enum missing %q", want)
+ }
+ }
+ return
+ }
+ t.Fatal("validate_sql not found")
+}
+
+func TestValidateSQLInternal_AllAdvertisedDialects(t *testing.T) {
+ for _, d := range supportedDialects {
+ t.Run(d, func(t *testing.T) {
+ res, err := validateSQLInternal("SELECT id FROM users WHERE id = 1", d)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if res["valid"] != true {
+ t.Errorf("dialect %q: valid = %v, error = %v", d, res["valid"], res["error"])
+ }
+ })
+ }
+}
+
+func TestApps_CallTool_StructuredContentMatchesText(t *testing.T) {
+ c, _ := newAppsClient(t, DefaultConfig())
+ const q = "SELECT u.id, COUNT(o.id) FROM users u JOIN orders o ON o.user_id = u.id WHERE u.name = 'x' OR 1=1 GROUP BY u.id"
+
+ tests := []struct {
+ tool string
+ args map[string]any
+ }{
+ {"validate_sql", map[string]any{"sql": q}},
+ {"validate_sql", map[string]any{"sql": "SELEC broken", "dialect": "postgresql"}},
+ {"format_sql", map[string]any{"sql": q, "uppercase_keywords": true}},
+ {"parse_sql", map[string]any{"sql": q}},
+ {"extract_metadata", map[string]any{"sql": q}},
+ {"security_scan", map[string]any{"sql": q}},
+ {"lint_sql", map[string]any{"sql": q}},
+ {"analyze_sql", map[string]any{"sql": q}},
+ {"analyze_sql", map[string]any{"sql": "SELEC broken"}},
+ }
+ for _, tt := range tests {
+ t.Run(tt.tool, func(t *testing.T) {
+ req := mcpmcp.CallToolRequest{}
+ req.Params.Name = tt.tool
+ req.Params.Arguments = tt.args
+ res, err := c.CallTool(context.Background(), req)
+ if err != nil {
+ t.Fatalf("CallTool: %v", err)
+ }
+ if res.IsError {
+ t.Fatalf("unexpected tool error: %+v", res.Content)
+ }
+ if res.StructuredContent == nil {
+ t.Fatal("structuredContent missing")
+ }
+ if len(res.Content) != 1 {
+ t.Fatalf("expected exactly 1 content item, got %d", len(res.Content))
+ }
+ tc, ok := res.Content[0].(mcpmcp.TextContent)
+ if !ok {
+ t.Fatalf("expected TextContent, got %T", res.Content[0])
+ }
+ var fromText any
+ if err := json.Unmarshal([]byte(tc.Text), &fromText); err != nil {
+ t.Fatalf("text content is not JSON: %v", err)
+ }
+ fromStructured := metaMap(t, res.StructuredContent)
+ if !reflect.DeepEqual(fromText, any(fromStructured)) {
+ t.Errorf("structuredContent != text JSON\ntext: %s\nstructured: %v", tc.Text, fromStructured)
+ }
+ })
+ }
+}
+
+// TestToolResult_TextIsByteIdentical guards backward compatibility: the text
+// content must stay exactly what json.Marshal produced before structured
+// content was added.
+func TestToolResult_TextIsByteIdentical(t *testing.T) {
+ data := map[string]any{"valid": true, "dialect": "postgresql", "n": 3, "list": []string{"a", "b"}}
+ want, err := json.Marshal(data)
+ if err != nil {
+ t.Fatalf("marshal: %v", err)
+ }
+ res, err := toolResult(data)
+ if err != nil {
+ t.Fatalf("toolResult: %v", err)
+ }
+ tc, ok := res.Content[0].(mcpmcp.TextContent)
+ if !ok {
+ t.Fatalf("expected TextContent, got %T", res.Content[0])
+ }
+ if tc.Text != string(want) {
+ t.Errorf("text = %s, want %s", tc.Text, want)
+ }
+ if tc.Type != "text" {
+ t.Errorf("content type = %q, want text", tc.Type)
+ }
+ sc, ok := res.StructuredContent.(map[string]any)
+ if !ok {
+ t.Fatalf("structuredContent is %T, want map[string]any", res.StructuredContent)
+ }
+ if !reflect.DeepEqual(sc, data) {
+ t.Errorf("structuredContent = %v, want %v", sc, data)
+ }
+}
+
+func TestServer_ToolCount(t *testing.T) {
+ if got := New(DefaultConfig()).ToolCount(); got != 7 {
+ t.Errorf("ToolCount() = %d, want 7", got)
+ }
+}
diff --git a/pkg/mcp/config.go b/pkg/mcp/config.go
index 5a29f30a..0b56ec57 100644
--- a/pkg/mcp/config.go
+++ b/pkg/mcp/config.go
@@ -30,6 +30,8 @@
// GOSQLX_MCP_HOST bind host (default: 127.0.0.1)
// GOSQLX_MCP_PORT bind port (default: 8080)
// GOSQLX_MCP_AUTH_TOKEN bearer token; empty disables auth
+// GOSQLX_MCP_OPENAI_APPS_CHALLENGE ChatGPT app domain-verification token,
+// served at /.well-known/openai-apps-challenge
package mcp
import (
@@ -54,6 +56,12 @@ type Config struct {
// When non-empty every request must carry "Authorization: Bearer ".
// Source: GOSQLX_MCP_AUTH_TOKEN (default: "" - auth disabled)
AuthToken string
+
+ // OpenAIAppsChallenge is the domain-verification token issued by the
+ // OpenAI Platform when submitting a ChatGPT app. When non-empty it is
+ // served as plain text at /.well-known/openai-apps-challenge.
+ // Source: GOSQLX_MCP_OPENAI_APPS_CHALLENGE (default: "" - not served)
+ OpenAIAppsChallenge string
}
// LoadConfig reads configuration from environment variables, applying defaults
@@ -80,6 +88,10 @@ func LoadConfig() (*Config, error) {
cfg.AuthToken = v
}
+ if v := strings.TrimSpace(os.Getenv("GOSQLX_MCP_OPENAI_APPS_CHALLENGE")); v != "" {
+ cfg.OpenAIAppsChallenge = v
+ }
+
return cfg, nil
}
diff --git a/pkg/mcp/config_test.go b/pkg/mcp/config_test.go
index 09ed8320..96d8e286 100644
--- a/pkg/mcp/config_test.go
+++ b/pkg/mcp/config_test.go
@@ -119,3 +119,26 @@ func TestDefaultConfig(t *testing.T) {
t.Error("AuthEnabled() = true, want false")
}
}
+
+func TestLoadConfig_OpenAIAppsChallenge(t *testing.T) {
+ tests := []struct {
+ name string
+ env string
+ want string
+ }{
+ {name: "unset", env: "", want: ""},
+ {name: "trimmed", env: " tok-123\n", want: "tok-123"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Setenv("GOSQLX_MCP_OPENAI_APPS_CHALLENGE", tt.env)
+ cfg, err := LoadConfig()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if cfg.OpenAIAppsChallenge != tt.want {
+ t.Errorf("OpenAIAppsChallenge = %q, want %q", cfg.OpenAIAppsChallenge, tt.want)
+ }
+ })
+ }
+}
diff --git a/pkg/mcp/server.go b/pkg/mcp/server.go
index ed288fdc..0b304c5b 100644
--- a/pkg/mcp/server.go
+++ b/pkg/mcp/server.go
@@ -19,37 +19,78 @@ import (
"fmt"
"log"
"net/http"
+ "time"
"github.com/mark3labs/mcp-go/mcp"
mcpserver "github.com/mark3labs/mcp-go/server"
+
+ "github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
)
-// Server wraps the MCP server with all GoSQLX tools registered.
+// Server wraps the MCP server with all GoSQLX tools and UI resources registered.
type Server struct {
cfg *Config
mcpSrv *mcpserver.MCPServer
}
-// New creates a Server with all 7 GoSQLX tools registered.
+// serverInstructions is returned in the initialize result so the client
+// model knows when to reach for each tool.
+const serverInstructions = `GoSQLX parses, validates, formats, lints, and security-scans SQL locally. ` +
+ `All tools are read-only, deterministic, and never connect to a database or execute SQL.
+
+Pick the narrowest tool for the request:
+- analyze_sql: one-shot review of a query (validity, statement types, tables/columns/functions, security findings, lint violations, formatted SQL). Prefer it when the user asks to "review", "check", or "analyze" SQL; hosts that support MCP Apps render it as an interactive report.
+- validate_sql: is the SQL syntactically valid, optionally for a specific dialect (postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, clickhouse).
+- format_sql: pretty-print SQL (indent size, keyword casing, trailing semicolon).
+- parse_sql: statement count and AST statement types.
+- extract_metadata: tables, columns, and functions referenced.
+- security_scan: SQL-injection pattern detection (tautologies, UNION-based, stacked queries, comment bypass, ...), with severities.
+- lint_sql: style rules L001-L010 (whitespace, indentation, line length, alignment, keyword case, comma placement, aliasing).
+
+Pass the SQL text exactly as the user wrote it. Results are JSON; summarise them for the user rather than repeating them verbatim.`
+
+// New creates a Server with all GoSQLX tools and the analysis widget resource registered.
func New(cfg *Config) *Server {
s := &Server{cfg: cfg}
s.mcpSrv = mcpserver.NewMCPServer(
"gosqlx-mcp",
- "1.14.0",
+ gosqlx.Version,
mcpserver.WithToolCapabilities(false),
+ mcpserver.WithResourceCapabilities(false, false),
+ mcpserver.WithInstructions(serverInstructions),
)
s.registerTools()
+ s.registerResources()
return s
}
+// ToolCount reports how many tools are registered.
+func (s *Server) ToolCount() int {
+ return len(s.mcpSrv.ListTools())
+}
+
// Handler returns the HTTP handler chain (MCP server + auth middleware)
// without starting an HTTP server. Use this when you need to wrap
// the handler with additional middleware (e.g., rate limiting).
func (s *Server) Handler() http.Handler {
- streamSrv := mcpserver.NewStreamableHTTPServer(s.mcpSrv)
+ streamSrv := s.newStreamableHTTPServer()
return BearerAuthMiddleware(s.cfg, streamSrv)
}
+// SessionIdleTTL is how long an idle MCP session's transport state is kept.
+// Clients that disconnect without sending DELETE (the common case for hosted
+// assistants) would otherwise leave their session state in memory until the
+// process restarts. The privacy policy on gosqlx.dev cites this value.
+const SessionIdleTTL = 30 * time.Minute
+
+// newStreamableHTTPServer builds the streamable HTTP transport with idle
+// session sweeping enabled.
+func (s *Server) newStreamableHTTPServer() *mcpserver.StreamableHTTPServer {
+ return mcpserver.NewStreamableHTTPServer(s.mcpSrv,
+ mcpserver.WithSessionIdleTTL(SessionIdleTTL),
+ )
+}
+
// Cfg returns the server configuration.
func (s *Server) Cfg() *Config {
return s.cfg
@@ -64,7 +105,7 @@ func (s *Server) MCPServer() *mcpserver.MCPServer {
// Start binds to cfg.Addr() and serves using streamable HTTP transport.
// It blocks until ctx is cancelled or a fatal error occurs.
func (s *Server) Start(ctx context.Context) error {
- streamSrv := mcpserver.NewStreamableHTTPServer(s.mcpSrv)
+ streamSrv := s.newStreamableHTTPServer()
wrapped := BearerAuthMiddleware(s.cfg, streamSrv)
httpSrv := &http.Server{
@@ -84,36 +125,62 @@ func (s *Server) Start(ctx context.Context) error {
return nil
}
+// readOnlyAnnotations are shared by every GoSQLX tool: pure local computation
+// on the SQL text, no side effects, no network or database access. ChatGPT
+// requires readOnly/destructive/openWorld to be set explicitly.
+func readOnlyAnnotations(title string) []mcp.ToolOption {
+ return []mcp.ToolOption{
+ mcp.WithTitleAnnotation(title),
+ mcp.WithReadOnlyHintAnnotation(true),
+ mcp.WithDestructiveHintAnnotation(false),
+ mcp.WithIdempotentHintAnnotation(true),
+ mcp.WithOpenWorldHintAnnotation(false),
+ }
+}
+
+// newTool assembles a tool from shared annotations, _meta and its own options.
+func newTool(name, title, description string, meta *mcp.Meta, opts ...mcp.ToolOption) mcp.Tool {
+ all := make([]mcp.ToolOption, 0, len(opts)+7)
+ all = append(all, mcp.WithDescription(description))
+ all = append(all, readOnlyAnnotations(title)...)
+ all = append(all, withToolMeta(meta))
+ all = append(all, opts...)
+ return mcp.NewTool(name, all...)
+}
+
+// supportedDialects lists the dialect values accepted by validate_sql. Each
+// one is accepted by gosqlx.ParseWithDialect.
+var supportedDialects = []string{
+ "generic", "postgresql", "mysql", "mariadb", "sqlserver",
+ "oracle", "sqlite", "snowflake", "clickhouse",
+}
+
// registerTools adds all 7 GoSQLX tools with their JSON Schema definitions.
func (s *Server) registerTools() {
- // validate_sql
+ cfg := s.cfg
+
s.mcpSrv.AddTool(
- mcp.NewTool("validate_sql",
- mcp.WithDescription("Validate SQL syntax. Returns {valid: bool, error?: string, dialect?: string}."),
- mcp.WithReadOnlyHintAnnotation(true),
- mcp.WithDestructiveHintAnnotation(false),
- mcp.WithIdempotentHintAnnotation(true),
- mcp.WithOpenWorldHintAnnotation(false),
+ newTool("validate_sql", "Validate SQL",
+ "Use this when the user wants to know whether a SQL statement is syntactically valid, "+
+ "optionally for a specific dialect. Returns {valid: bool, error?: string, dialect?: string}.",
+ toolMeta(cfg, "Validating SQL…", "Validated SQL", false),
mcp.WithString("sql",
mcp.Required(),
mcp.Description("The SQL string to validate"),
),
mcp.WithString("dialect",
- mcp.Description("SQL dialect: generic, mysql, postgresql, sqlite, sqlserver, oracle, snowflake"),
- mcp.Enum("generic", "mysql", "postgresql", "sqlite", "sqlserver", "oracle", "snowflake"),
+ mcp.Description("SQL dialect: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, clickhouse"),
+ mcp.Enum(supportedDialects...),
),
),
handleValidateSQL,
)
- // format_sql
s.mcpSrv.AddTool(
- mcp.NewTool("format_sql",
- mcp.WithDescription("Format SQL with configurable indentation and keyword casing."),
- mcp.WithReadOnlyHintAnnotation(true),
- mcp.WithDestructiveHintAnnotation(false),
- mcp.WithIdempotentHintAnnotation(true),
- mcp.WithOpenWorldHintAnnotation(false),
+ newTool("format_sql", "Format SQL",
+ "Use this when the user wants SQL pretty-printed or reformatted, with configurable "+
+ "indentation, keyword casing, and trailing semicolon. Returns {formatted_sql, options}.",
+ toolMeta(cfg, "Formatting SQL…", "Formatted SQL", false),
mcp.WithString("sql",
mcp.Required(),
mcp.Description("The SQL string to format"),
@@ -131,14 +198,11 @@ func (s *Server) registerTools() {
handleFormatSQL,
)
- // parse_sql
s.mcpSrv.AddTool(
- mcp.NewTool("parse_sql",
- mcp.WithDescription("Parse SQL and return an AST summary: statement count and types."),
- mcp.WithReadOnlyHintAnnotation(true),
- mcp.WithDestructiveHintAnnotation(false),
- mcp.WithIdempotentHintAnnotation(true),
- mcp.WithOpenWorldHintAnnotation(false),
+ newTool("parse_sql", "Parse SQL",
+ "Use this when the user wants to know how SQL parses: returns an AST summary with "+
+ "the statement count and the type of each statement.",
+ toolMeta(cfg, "Parsing SQL…", "Parsed SQL", false),
mcp.WithString("sql",
mcp.Required(),
mcp.Description("The SQL string to parse"),
@@ -147,14 +211,11 @@ func (s *Server) registerTools() {
handleParseSQL,
)
- // extract_metadata
s.mcpSrv.AddTool(
- mcp.NewTool("extract_metadata",
- mcp.WithDescription("Extract tables, columns, and functions referenced in SQL."),
- mcp.WithReadOnlyHintAnnotation(true),
- mcp.WithDestructiveHintAnnotation(false),
- mcp.WithIdempotentHintAnnotation(true),
- mcp.WithOpenWorldHintAnnotation(false),
+ newTool("extract_metadata", "Extract SQL metadata",
+ "Use this when the user wants the tables, columns, and functions a SQL query references "+
+ "(e.g. for lineage or impact analysis). Returns {tables, columns, functions}.",
+ toolMeta(cfg, "Extracting tables and columns…", "Extracted SQL metadata", false),
mcp.WithString("sql",
mcp.Required(),
mcp.Description("The SQL string to analyze"),
@@ -163,14 +224,11 @@ func (s *Server) registerTools() {
handleExtractMetadata,
)
- // security_scan
s.mcpSrv.AddTool(
- mcp.NewTool("security_scan",
- mcp.WithDescription("Scan SQL for injection patterns: tautologies, UNION attacks, stacked queries, comment bypasses, and more."),
- mcp.WithReadOnlyHintAnnotation(true),
- mcp.WithDestructiveHintAnnotation(false),
- mcp.WithIdempotentHintAnnotation(true),
- mcp.WithOpenWorldHintAnnotation(false),
+ newTool("security_scan", "Scan SQL for injection",
+ "Use this when the user wants SQL checked for injection patterns: tautologies, UNION attacks, "+
+ "stacked queries, comment bypasses, and more. Returns findings with severity counts.",
+ toolMeta(cfg, "Scanning SQL for injection patterns…", "Scanned SQL", false),
mcp.WithString("sql",
mcp.Required(),
mcp.Description("The SQL string to scan"),
@@ -179,14 +237,14 @@ func (s *Server) registerTools() {
handleSecurityScan,
)
- // lint_sql
s.mcpSrv.AddTool(
- mcp.NewTool("lint_sql",
- mcp.WithDescription("Lint SQL against all 10 GoSQLX style rules (L001–L010)."),
- mcp.WithReadOnlyHintAnnotation(true),
- mcp.WithDestructiveHintAnnotation(false),
- mcp.WithIdempotentHintAnnotation(true),
- mcp.WithOpenWorldHintAnnotation(false),
+ newTool("lint_sql", "Lint SQL",
+ "Use this when the user wants SQL style feedback. Applies the same 10 rules as `gosqlx lint`: "+
+ "L001 trailing whitespace, L002 mixed indentation, L003 consecutive blank lines, "+
+ "L004 indentation depth, L005 line length (>100), L006 column alignment, L007 keyword case (upper), "+
+ "L008 comma placement (trailing), L009 aliasing consistency, L010 redundant whitespace. "+
+ "Returns {violation_count, violations}.",
+ toolMeta(cfg, "Linting SQL…", "Linted SQL", false),
mcp.WithString("sql",
mcp.Required(),
mcp.Description("The SQL string to lint"),
@@ -195,14 +253,12 @@ func (s *Server) registerTools() {
handleLintSQL,
)
- // analyze_sql
s.mcpSrv.AddTool(
- mcp.NewTool("analyze_sql",
- mcp.WithDescription("Run all 6 analysis tools concurrently and return a composite report (validate, parse, metadata, security, lint, format)."),
- mcp.WithReadOnlyHintAnnotation(true),
- mcp.WithDestructiveHintAnnotation(false),
- mcp.WithIdempotentHintAnnotation(true),
- mcp.WithOpenWorldHintAnnotation(false),
+ newTool("analyze_sql", "Analyze SQL",
+ "Use this when the user wants a complete review of a SQL query. Runs validate, parse, metadata, "+
+ "security, lint, and format concurrently and returns one composite report; "+
+ "hosts that support MCP Apps render it as an interactive report.",
+ toolMeta(cfg, "Analyzing SQL…", "Analyzed SQL", true),
mcp.WithString("sql",
mcp.Required(),
mcp.Description("The SQL string to analyze"),
diff --git a/pkg/mcp/tools.go b/pkg/mcp/tools.go
index 481b4417..dcec3acd 100644
--- a/pkg/mcp/tools.go
+++ b/pkg/mcp/tools.go
@@ -239,12 +239,16 @@ func newFullLinter() *linter.Linter {
}
// toolResult marshals a map[string]any result into an MCP CallToolResult.
+//
+// The JSON is returned twice: as the text content (unchanged, for clients that
+// only read content) and as structuredContent, which MCP Apps hosts and
+// ChatGPT hand to both the model and the UI widget.
func toolResult(data map[string]any) (*mcpmcp.CallToolResult, error) {
b, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("failed to marshal result: %w", err)
}
- return mcpmcp.NewToolResultText(string(b)), nil
+ return mcpmcp.NewToolResultStructured(data, string(b)), nil
}
// ---------------------------------------------------------------------------
diff --git a/pkg/mcp/widget/analysis.html b/pkg/mcp/widget/analysis.html
new file mode 100644
index 00000000..81b5a7b1
--- /dev/null
+++ b/pkg/mcp/widget/analysis.html
@@ -0,0 +1,446 @@
+
+
+
+
+
+GoSQLX analysis
+
+
+
+
+Waiting for the GoSQLX analysis…
+
+
+
diff --git a/render.yaml b/render.yaml
index 6010b31c..87e195ec 100644
--- a/render.yaml
+++ b/render.yaml
@@ -10,4 +10,9 @@ services:
value: "0.0.0.0"
- key: GOSQLX_MCP_PORT
value: "8080"
+ # Domain-verification token for the ChatGPT app directory, issued by the
+ # OpenAI Platform during submission. Set the value in the Render
+ # dashboard; it is served at /.well-known/openai-apps-challenge.
+ - key: GOSQLX_MCP_OPENAI_APPS_CHALLENGE
+ sync: false
healthCheckPath: /health
From ed67de5525f43bfb2380868d43d184658d80b3d8 Mon Sep 17 00:00:00 2001
From: Ajit Pratap Singh
Date: Tue, 20 Oct 2026 00:42:56 +0530
Subject: [PATCH 2/8] feat(plugin): add GoSQLX Claude Code plugin and
marketplace
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
Claude-Session: https://claude.ai/code/session_01L2wnDjnjXs2u8baaf2PC6w
---
.claude-plugin/marketplace.json | 21 +++
plugins/gosqlx/.claude-plugin/plugin.json | 16 ++
plugins/gosqlx/.mcp.json | 8 +
plugins/gosqlx/README.md | 155 ++++++++++++++++++
plugins/gosqlx/agents/sql-reviewer.md | 30 ++++
plugins/gosqlx/commands/analyze.md | 11 ++
plugins/gosqlx/commands/format.md | 11 ++
plugins/gosqlx/commands/lint.md | 11 ++
plugins/gosqlx/commands/security.md | 13 ++
plugins/gosqlx/hooks/hooks.json | 16 ++
plugins/gosqlx/hooks/validate-sql.sh | 45 +++++
plugins/gosqlx/skills/sql-analysis/SKILL.md | 37 +++++
plugins/gosqlx/skills/sql-dialects/SKILL.md | 37 +++++
plugins/gosqlx/skills/sql-formatting/SKILL.md | 36 ++++
plugins/gosqlx/skills/sql-linting/SKILL.md | 73 +++++++++
.../skills/sql-security-review/SKILL.md | 46 ++++++
16 files changed, 566 insertions(+)
create mode 100644 .claude-plugin/marketplace.json
create mode 100644 plugins/gosqlx/.claude-plugin/plugin.json
create mode 100644 plugins/gosqlx/.mcp.json
create mode 100644 plugins/gosqlx/README.md
create mode 100644 plugins/gosqlx/agents/sql-reviewer.md
create mode 100644 plugins/gosqlx/commands/analyze.md
create mode 100644 plugins/gosqlx/commands/format.md
create mode 100644 plugins/gosqlx/commands/lint.md
create mode 100644 plugins/gosqlx/commands/security.md
create mode 100644 plugins/gosqlx/hooks/hooks.json
create mode 100755 plugins/gosqlx/hooks/validate-sql.sh
create mode 100644 plugins/gosqlx/skills/sql-analysis/SKILL.md
create mode 100644 plugins/gosqlx/skills/sql-dialects/SKILL.md
create mode 100644 plugins/gosqlx/skills/sql-formatting/SKILL.md
create mode 100644 plugins/gosqlx/skills/sql-linting/SKILL.md
create mode 100644 plugins/gosqlx/skills/sql-security-review/SKILL.md
diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
new file mode 100644
index 00000000..dd5c9f2a
--- /dev/null
+++ b/.claude-plugin/marketplace.json
@@ -0,0 +1,21 @@
+{
+ "name": "gosqlx",
+ "description": "Official GoSQLX plugins for Claude Code: SQL validation, formatting, linting, security scanning and analysis.",
+ "owner": {
+ "name": "Ajit Pratap Singh",
+ "url": "https://github.com/ajitpratap0"
+ },
+ "plugins": [
+ {
+ "name": "gosqlx",
+ "source": "./plugins/gosqlx",
+ "description": "Validate, format, lint, analyze and security-scan SQL with GoSQLX (MCP tools, skills, slash commands, a SQL reviewer agent and a .sql validation hook).",
+ "version": "1.15.0",
+ "category": "development",
+ "tags": ["sql", "database", "linting", "formatting", "security", "mcp"],
+ "homepage": "https://gosqlx.dev",
+ "repository": "https://github.com/ajitpratap0/GoSQLX",
+ "license": "Apache-2.0"
+ }
+ ]
+}
diff --git a/plugins/gosqlx/.claude-plugin/plugin.json b/plugins/gosqlx/.claude-plugin/plugin.json
new file mode 100644
index 00000000..f3db6b0f
--- /dev/null
+++ b/plugins/gosqlx/.claude-plugin/plugin.json
@@ -0,0 +1,16 @@
+{
+ "name": "gosqlx",
+ "displayName": "GoSQLX",
+ "version": "1.15.0",
+ "description": "SQL validation, formatting, linting, injection scanning and analysis for Claude Code, powered by the GoSQLX parser (multi-dialect).",
+ "author": {
+ "name": "Ajit Pratap Singh",
+ "url": "https://github.com/ajitpratap0"
+ },
+ "homepage": "https://gosqlx.dev",
+ "repository": "https://github.com/ajitpratap0/GoSQLX",
+ "documentationUrl": "https://github.com/ajitpratap0/GoSQLX/blob/main/plugins/gosqlx/README.md",
+ "supportUrl": "https://github.com/ajitpratap0/GoSQLX/issues",
+ "license": "Apache-2.0",
+ "keywords": ["sql", "parser", "linter", "formatter", "sql-injection", "postgresql", "mysql", "snowflake", "mcp"]
+}
diff --git a/plugins/gosqlx/.mcp.json b/plugins/gosqlx/.mcp.json
new file mode 100644
index 00000000..caa434fe
--- /dev/null
+++ b/plugins/gosqlx/.mcp.json
@@ -0,0 +1,8 @@
+{
+ "mcpServers": {
+ "gosqlx": {
+ "type": "http",
+ "url": "${GOSQLX_MCP_URL:-https://mcp.gosqlx.dev/mcp}"
+ }
+ }
+}
diff --git a/plugins/gosqlx/README.md b/plugins/gosqlx/README.md
new file mode 100644
index 00000000..7044242f
--- /dev/null
+++ b/plugins/gosqlx/README.md
@@ -0,0 +1,155 @@
+# GoSQLX plugin for Claude Code
+
+The official [GoSQLX](https://gosqlx.dev) plugin for Claude Code. It gives Claude SQL validation, formatting, linting, SQL injection scanning and analysis, backed by the GoSQLX parser.
+
+## Install
+
+In Claude Code:
+
+```text
+/plugin marketplace add ajitpratap0/GoSQLX
+/plugin install gosqlx@gosqlx
+```
+
+From a shell:
+
+```sh
+claude plugin marketplace add ajitpratap0/GoSQLX
+claude plugin install gosqlx@gosqlx
+```
+
+The marketplace lives at the root of the GoSQLX repository. To avoid checking out the whole repository, add it with a sparse checkout: `claude plugin marketplace add ajitpratap0/GoSQLX --sparse .claude-plugin plugins`.
+
+Run `/reload-plugins` (or restart Claude Code) and check `/mcp`. The server appears as `plugin:gosqlx:gosqlx`.
+
+## What's included
+
+| Component | Name | What it does |
+|---|---|---|
+| MCP server | `gosqlx` | Remote GoSQLX MCP server at `https://mcp.gosqlx.dev/mcp`. No install, no API key |
+| Skill | `sql-analysis` | Full review via `analyze_sql`, explained in plain language |
+| Skill | `sql-formatting` | Formatting via `format_sql` |
+| Skill | `sql-security-review` | Injection scan via `security_scan`, plus review of code that builds SQL strings |
+| Skill | `sql-linting` | Lint via `lint_sql`; explains rules L001-L030 |
+| Skill | `sql-dialects` | Dialect validation via `validate_sql` |
+| Command | `/gosqlx:analyze` | Analyze a SQL string or `.sql` file |
+| Command | `/gosqlx:format` | Format a SQL string or `.sql` file |
+| Command | `/gosqlx:lint` | Lint a SQL string or `.sql` file |
+| Command | `/gosqlx:security` | Injection review of SQL or a source file |
+| Agent | `sql-reviewer` | Read-only reviewer for SQL files, migrations and SQL-building code |
+| Hook | `PostToolUse` | After Claude writes or edits a `.sql` file, validates it with the local `gosqlx` CLI if it is installed |
+
+MCP tools (all read-only): `validate_sql`, `format_sql`, `parse_sql`, `extract_metadata`, `security_scan`, `lint_sql`, `analyze_sql`. In Claude Code they are named `mcp__plugin_gosqlx_gosqlx__`.
+
+## Usage
+
+```text
+/gosqlx:analyze SELECT * FROM users WHERE id = 1 OR 1=1
+/gosqlx:format db/queries/report.sql
+/gosqlx:lint "select a,b from t where x=1"
+/gosqlx:security internal/store/users.go
+@agent-gosqlx:sql-reviewer review the migrations in this branch
+```
+
+The skills also trigger from plain requests, for example "is this query valid on Snowflake?", "format this SQL", "explain lint rule L019" or "check this handler for SQL injection".
+
+### Things to know
+
+- `lint_sql` and `gosqlx lint` apply the style rules **L001-L010**. Rules L011-L030 (safety, performance, naming) run only through the Go library (`gosqlx.Lint`). See [LINTING_RULES.md](https://github.com/ajitpratap0/GoSQLX/blob/main/docs/LINTING_RULES.md).
+- `validate_sql` accepts these dialects: `generic`, `mysql`, `postgresql`, `sqlite`, `sqlserver`, `oracle`, `snowflake`. Newer server builds also accept `mariadb` and `clickhouse`; if the connected server's tool schema does not list them, use the CLI: `gosqlx validate --dialect clickhouse file.sql`.
+- The public server is rate limited: a burst of 10 requests per second and 120 weighted requests per minute per IP. `analyze_sql` costs more than the other tools.
+
+## The validation hook
+
+The hook runs `hooks/validate-sql.sh` after `Write`, `Edit` and `MultiEdit`. It is best-effort and never blocks:
+
+- It does nothing unless the edited file ends in `.sql` and the `gosqlx` CLI is on `PATH`.
+- If `gosqlx validate` reports a syntax error, the error is passed back to Claude so it can fix the file.
+- It reads the hook input with `jq` when available, with a `sed` fallback otherwise.
+- To turn it off, set `GOSQLX_HOOK_DISABLE=1` in your environment.
+
+Install the CLI to enable it:
+
+```sh
+go install github.com/ajitpratap0/GoSQLX/cmd/gosqlx@latest
+```
+
+The CLI validates with its default dialect. Dialect-specific files can produce false positives.
+
+## Privacy
+
+By default the plugin uses the hosted server `https://mcp.gosqlx.dev/mcp`. **Any SQL that Claude passes to a GoSQLX tool is sent to that server.** Don't send SQL that contains secrets, credentials or personal data. For proprietary schemas or queries, use local mode.
+
+The tools only parse and analyze text. They never connect to or execute anything against a database.
+
+## Local / private mode
+
+Install the MCP server binary:
+
+```sh
+go install github.com/ajitpratap0/GoSQLX/cmd/gosqlx-mcp@latest
+```
+
+**Option A: keep the plugin and point it at a local HTTP server.** The plugin's server URL honours `GOSQLX_MCP_URL`:
+
+```sh
+gosqlx-mcp # listens on 127.0.0.1:8080 (GOSQLX_MCP_HOST / GOSQLX_MCP_PORT)
+GOSQLX_MCP_URL=http://127.0.0.1:8080/mcp claude
+```
+
+Tool names stay the same, so the skills, commands and agent work unchanged.
+
+**Option B: run the server over stdio.** Add it as a separate MCP server:
+
+```sh
+claude mcp add gosqlx-local -- gosqlx-mcp --stdio
+```
+
+Or add it to a project `.mcp.json`:
+
+```json
+{
+ "mcpServers": {
+ "gosqlx-local": {
+ "command": "gosqlx-mcp",
+ "args": ["--stdio"]
+ }
+ }
+}
+```
+
+With option B, the tools are named `mcp__gosqlx-local__`. The skills refer to tools by their short names (`analyze_sql` and so on), so Claude still finds them, but you may be asked for permission the first time each one runs. To keep everything local, also disable the plugin's remote server in `/mcp`.
+
+**CLI only:** with no MCP server at all, every skill, command and the agent fall back to the local `gosqlx` CLI (`validate`, `format`, `lint`, `analyze`).
+
+## Claude.ai and Claude Desktop (custom connector)
+
+Claude.ai, Claude Desktop and Cowork can't run Claude Code plugins, but they can use the same GoSQLX tools as a remote MCP custom connector.
+
+**Pro / Max:**
+
+1. Open **Customize > Connectors**. In some versions this is under **Settings > Connectors**.
+2. Click **+ Add**, then **Add custom connector**.
+3. Name: `GoSQLX`. Remote MCP server URL: `https://mcp.gosqlx.dev/mcp`.
+4. Authentication: **No sign in**. The server needs no auth.
+5. Click **Add**.
+
+**Team / Enterprise:** an Owner adds the connector under **Organization settings > Connectors > Add > Custom > Web** with the same name and URL. Members then connect it from **Customize > Connectors**.
+
+**In a chat:** click **+** in the lower-left corner of the composer, open **Connectors**, and turn on **GoSQLX**.
+
+On the Free plan you can add one custom connector. The same privacy note applies: SQL you send through the connector goes to `mcp.gosqlx.dev`.
+
+Claude Desktop can also run the server locally over stdio through `claude_desktop_config.json` (`"command": "gosqlx-mcp", "args": ["--stdio"]`). See the [MCP guide](https://github.com/ajitpratap0/GoSQLX/blob/main/docs/MCP_GUIDE.md).
+
+## Development
+
+```sh
+claude plugin validate . # from the repository root (marketplace)
+claude plugin validate plugins/gosqlx # the plugin itself
+claude --plugin-dir ./plugins/gosqlx # try it without installing
+```
+
+## License
+
+Apache-2.0. See [LICENSE](https://github.com/ajitpratap0/GoSQLX/blob/main/LICENSE).
diff --git a/plugins/gosqlx/agents/sql-reviewer.md b/plugins/gosqlx/agents/sql-reviewer.md
new file mode 100644
index 00000000..dd015bdf
--- /dev/null
+++ b/plugins/gosqlx/agents/sql-reviewer.md
@@ -0,0 +1,30 @@
+---
+name: sql-reviewer
+description: Read-only reviewer for SQL files, migrations and application code that builds SQL. Uses GoSQLX tools to validate syntax, lint style, scan for injection patterns and check dialect compatibility, then returns prioritized findings. Use after SQL or data-access code is written or changed, or when asked to review database queries.
+model: sonnet
+tools: Read, Grep, Glob, Bash, mcp__plugin_gosqlx_gosqlx__validate_sql, mcp__plugin_gosqlx_gosqlx__format_sql, mcp__plugin_gosqlx_gosqlx__parse_sql, mcp__plugin_gosqlx_gosqlx__extract_metadata, mcp__plugin_gosqlx_gosqlx__security_scan, mcp__plugin_gosqlx_gosqlx__lint_sql, mcp__plugin_gosqlx_gosqlx__analyze_sql
+---
+
+You are a SQL reviewer. You review; you never edit files, apply fixes, or run SQL against any database.
+
+## Scope
+
+Work on the files or diff you are given. If none were named, use `git diff --name-only` and `git status --short` to find changed `.sql` files, migrations, and source files that contain SQL (search for `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `CREATE`, query-builder and raw-query calls).
+
+## For each SQL statement
+
+1. `analyze_sql` for the combined report (validity, statement types, tables/columns, security, lint L001-L010).
+2. `validate_sql` with `dialect` when the target database is known (`generic`, `mysql`, `postgresql`, `sqlite`, `sqlserver`, `oracle`, `snowflake`, plus `mariadb` and `clickhouse` when the tool schema lists them; otherwise use `gosqlx validate --dialect` for those two). Infer it from drivers, connection strings, or migration tooling.
+3. Your own checks GoSQLX does not run over MCP: `DELETE`/`UPDATE` without `WHERE` (L011/L012), `DROP` without `IF EXISTS` (L013), `NOT IN` against a nullable subquery (L019), `LIMIT` without `ORDER BY` (L028), `SELECT *` (L016), implicit cross joins (L023), missing indexes implied by new predicates, and destructive or locking DDL in migrations.
+
+## For code that builds SQL
+
+Trace user-controlled values into query text. Flag concatenation, interpolation (`fmt.Sprintf`, f-strings, template literals, `String.format`), raw-query ORM calls, and dynamic identifiers. Reconstruct a hostile instance (for example `' OR 1=1 --`) and run `security_scan` on it to demonstrate the issue. Recommend bind parameters for values and allowlists for identifiers.
+
+## Without the MCP tools
+
+If the `gosqlx` MCP tools are unavailable, use the CLI through Bash, read-only commands only: `gosqlx validate [--dialect d] `, `gosqlx lint `, `gosqlx analyze --all `, `gosqlx format ` (stdout). Never pass `-i`, `--in-place` or `--auto-fix`. If neither is available, review manually and say so.
+
+## Output
+
+Return findings ordered by severity (Critical, High, Medium, Low). For each: `file:line`, the problem, evidence (tool output or code excerpt), and a concrete fix. Label which findings come from GoSQLX tools and which are your judgment. End with a one-line verdict. If nothing is wrong, say so plainly.
diff --git a/plugins/gosqlx/commands/analyze.md b/plugins/gosqlx/commands/analyze.md
new file mode 100644
index 00000000..f13991a2
--- /dev/null
+++ b/plugins/gosqlx/commands/analyze.md
@@ -0,0 +1,11 @@
+---
+description: Full GoSQLX analysis of a SQL string or .sql file (validity, metadata, security, lint, formatting)
+argument-hint: ""
+allowed-tools: mcp__plugin_gosqlx_gosqlx__analyze_sql mcp__plugin_gosqlx_gosqlx__validate_sql Read Bash(gosqlx analyze:*) Bash(gosqlx validate:*)
+---
+
+Analyze this SQL with GoSQLX: $ARGUMENTS
+
+If the argument is a path to an existing file, read it and use its contents as the SQL; otherwise treat the argument as SQL text. If it is empty, ask for the SQL.
+
+Call the `analyze_sql` MCP tool with the SQL, then follow the `sql-analysis` skill to report: validity (with line/column for errors), what the statement does, security findings by severity, lint violations by rule, and a short prioritized fix list. If the MCP server is unavailable, fall back to `gosqlx validate` and `gosqlx analyze --all` via Bash. Do not modify any file.
diff --git a/plugins/gosqlx/commands/format.md b/plugins/gosqlx/commands/format.md
new file mode 100644
index 00000000..00789e64
--- /dev/null
+++ b/plugins/gosqlx/commands/format.md
@@ -0,0 +1,11 @@
+---
+description: Format a SQL string or .sql file with GoSQLX
+argument-hint: ""
+allowed-tools: mcp__plugin_gosqlx_gosqlx__format_sql mcp__plugin_gosqlx_gosqlx__validate_sql Read Bash(gosqlx format:*)
+---
+
+Format this SQL with GoSQLX: $ARGUMENTS
+
+If the argument is a path to an existing file, read it and use its contents; otherwise treat the argument as SQL text. If it is empty, ask for the SQL.
+
+Call the `format_sql` MCP tool (2-space indent; set `uppercase_keywords` to true unless the input clearly uses lowercase keywords). Show the result in a `sql` code block. For a file, ask before overwriting it. If formatting fails, run `validate_sql` and report the syntax error. If the MCP server is unavailable, use `gosqlx format` via Bash (it prints to stdout; do not pass `-i` unless the user asked to edit the file).
diff --git a/plugins/gosqlx/commands/lint.md b/plugins/gosqlx/commands/lint.md
new file mode 100644
index 00000000..ff8092d9
--- /dev/null
+++ b/plugins/gosqlx/commands/lint.md
@@ -0,0 +1,11 @@
+---
+description: Lint a SQL string or .sql file with GoSQLX style rules (L001-L010)
+argument-hint: ""
+allowed-tools: mcp__plugin_gosqlx_gosqlx__lint_sql Read Bash(gosqlx lint:*)
+---
+
+Lint this SQL with GoSQLX: $ARGUMENTS
+
+If the argument is a path to an existing file, read it and use its contents; otherwise treat the argument as SQL text. If it is empty, ask for the SQL.
+
+Call the `lint_sql` MCP tool. Group the violations by rule ID, show each offending line with its line/column, and give the fix; note which rules `gosqlx lint --auto-fix` can fix (L001, L002, L003, L007, L010). Use the `sql-linting` skill for rule explanations. If the MCP server is unavailable, run `gosqlx lint` via Bash. Do not modify any file unless asked.
diff --git a/plugins/gosqlx/commands/security.md b/plugins/gosqlx/commands/security.md
new file mode 100644
index 00000000..a54dc55c
--- /dev/null
+++ b/plugins/gosqlx/commands/security.md
@@ -0,0 +1,13 @@
+---
+description: Scan a SQL string, .sql file, or SQL-building source file for injection risks with GoSQLX
+argument-hint: ""
+allowed-tools: mcp__plugin_gosqlx_gosqlx__security_scan Read Grep Glob Bash(gosqlx analyze:*) Bash(gosqlx lint:*)
+---
+
+Run a SQL injection review with GoSQLX on: $ARGUMENTS
+
+- If the argument is a `.sql` file or SQL text, call the `security_scan` MCP tool on the SQL and report findings by severity with pattern, risk and suggested fix.
+- If it is a source file (Go, Python, JS/TS, Java, PHP, ...), find every place that builds SQL from variables, explain which ones are injectable, scan reconstructed SQL with `security_scan` where useful, and recommend parameterized queries or allowlists.
+- If it is empty, review the SQL-related code changed in this session.
+
+Follow the `sql-security-review` skill. If the MCP server is unavailable, use `gosqlx analyze --security` via Bash. Do not modify any file.
diff --git a/plugins/gosqlx/hooks/hooks.json b/plugins/gosqlx/hooks/hooks.json
new file mode 100644
index 00000000..e1c8e91e
--- /dev/null
+++ b/plugins/gosqlx/hooks/hooks.json
@@ -0,0 +1,16 @@
+{
+ "hooks": {
+ "PostToolUse": [
+ {
+ "matcher": "Write|Edit|MultiEdit",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/validate-sql.sh\"",
+ "timeout": 30
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/plugins/gosqlx/hooks/validate-sql.sh b/plugins/gosqlx/hooks/validate-sql.sh
new file mode 100755
index 00000000..56e51549
--- /dev/null
+++ b/plugins/gosqlx/hooks/validate-sql.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+# GoSQLX PostToolUse hook: validate a .sql file after Claude writes or edits it.
+#
+# Best-effort by design: it never blocks a tool call. It exits 0 silently when
+# the edited file is not a .sql file, when the gosqlx CLI is not installed, or
+# when the hook input cannot be read. When gosqlx reports a syntax error it
+# exits 2 so the message on stderr is shown to Claude (the edit has already
+# been applied; PostToolUse cannot undo it).
+#
+# Disable it with GOSQLX_HOOK_DISABLE=1.
+
+[ "${GOSQLX_HOOK_DISABLE:-0}" = "1" ] && exit 0
+command -v gosqlx >/dev/null 2>&1 || exit 0
+
+input=$(cat 2>/dev/null) || exit 0
+[ -n "$input" ] || exit 0
+
+file=""
+if command -v jq >/dev/null 2>&1; then
+ file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null) || exit 0
+else
+ # Fallback without jq: take the first "file_path":"..." value. Give up on
+ # paths containing JSON escapes rather than risk reading the wrong file.
+ file=$(printf '%s' "$input" | tr -d '\n' |
+ sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
+ case $file in *\\*) exit 0 ;; esac
+fi
+
+[ -n "$file" ] || exit 0
+case $file in
+ *.sql|*.SQL) ;;
+ *) exit 0 ;;
+esac
+[ -f "$file" ] || exit 0
+
+if output=$(gosqlx validate -- "$file" 2>&1); then
+ exit 0
+fi
+
+{
+ printf 'GoSQLX validation failed for %s:\n' "$file"
+ printf '%s\n' "$output" | head -n 40
+ printf 'If this file targets a specific dialect, re-check with: gosqlx validate --dialect "%s"\n' "$file"
+} >&2
+exit 2
diff --git a/plugins/gosqlx/skills/sql-analysis/SKILL.md b/plugins/gosqlx/skills/sql-analysis/SKILL.md
new file mode 100644
index 00000000..f2a2f8fa
--- /dev/null
+++ b/plugins/gosqlx/skills/sql-analysis/SKILL.md
@@ -0,0 +1,37 @@
+---
+name: sql-analysis
+description: Full GoSQLX review of a SQL query or .sql file - syntax validity, statement types, referenced tables/columns/functions, injection patterns, style lint and a formatted version - followed by a plain-language explanation of what the SQL does and what to fix. Use when the user asks to analyze, review, check, explain or sanity-check SQL.
+argument-hint: "[SQL string or path to .sql file]"
+allowed-tools: mcp__plugin_gosqlx_gosqlx__analyze_sql mcp__plugin_gosqlx_gosqlx__validate_sql mcp__plugin_gosqlx_gosqlx__extract_metadata Read Bash(gosqlx analyze:*) Bash(gosqlx validate:*)
+---
+
+# SQL analysis with GoSQLX
+
+Input: `$ARGUMENTS` - a SQL string, or a path to a `.sql` file (read the file first). If nothing was given, use the SQL in the current conversation or ask for it.
+
+## Steps
+
+1. Call `analyze_sql` (MCP server `gosqlx`, tool `mcp__plugin_gosqlx_gosqlx__analyze_sql`) with `{"sql": ""}`. It runs validate, parse, metadata, security, lint and format concurrently and returns one report with those six sections.
+2. If the user names a dialect, also call `validate_sql` with `dialect` set (see the `sql-dialects` skill for valid values), because `analyze_sql` validates with the generic parser.
+3. For a large multi-statement file, it is fine to analyze statement by statement.
+
+## Report back
+
+- **Verdict**: valid or not. For a parse error, quote the error and point at the line/column.
+- **What it does**: statement types and the tables, columns and functions it touches (from the parse and metadata sections).
+- **Security**: each finding with severity (CRITICAL/HIGH/MEDIUM/LOW) and pattern. A clean scan means no known injection pattern was found, not that the calling code is safe - see `sql-security-review`.
+- **Style**: lint violations (rules L001-L010) grouped by rule, with the suggested fix.
+- **Formatted SQL**: show the formatted version only when it differs meaningfully.
+- **Recommendations**: a short prioritized list. Add your own judgment (missing indexes, `NOT IN` with NULLs, unbounded `DELETE`/`UPDATE`, `SELECT *`) and label it as your judgment, not GoSQLX output.
+
+## Fallback when the MCP tools are unavailable
+
+If the `gosqlx` MCP server is not connected, use the local CLI via Bash (install: `go install github.com/ajitpratap0/GoSQLX/cmd/gosqlx@latest`):
+
+```sh
+gosqlx validate path/to/query.sql # or: echo "SELECT ..." | gosqlx validate
+gosqlx analyze --all path/to/query.sql # security, performance and complexity report
+gosqlx analyze "SELECT * FROM users" # inline SQL
+```
+
+Never execute the SQL against a database as part of the analysis.
diff --git a/plugins/gosqlx/skills/sql-dialects/SKILL.md b/plugins/gosqlx/skills/sql-dialects/SKILL.md
new file mode 100644
index 00000000..19161fc5
--- /dev/null
+++ b/plugins/gosqlx/skills/sql-dialects/SKILL.md
@@ -0,0 +1,37 @@
+---
+name: sql-dialects
+description: Check whether SQL parses under a specific database dialect with GoSQLX (PostgreSQL, MySQL, SQL Server, Oracle, SQLite, Snowflake, generic; MariaDB and ClickHouse via the CLI) and explain dialect-specific syntax problems. Use when the user asks if SQL is valid for a particular database, is porting SQL between databases, or gets a syntax error that may be dialect-related.
+argument-hint: "[dialect] [SQL string or .sql file]"
+allowed-tools: mcp__plugin_gosqlx_gosqlx__validate_sql Read Bash(gosqlx validate:*) Bash(gosqlx transpile:*)
+---
+
+# Dialect validation with GoSQLX
+
+Input: `$ARGUMENTS` - usually a dialect name and SQL (string or `.sql` path). If no dialect is given, infer it from the project (driver imports, connection strings, migration tooling) or ask.
+
+## MCP tool
+
+Call `validate_sql` (`mcp__plugin_gosqlx_gosqlx__validate_sql`) with `sql` and `dialect`. Use only values listed in the tool's `dialect` enum. Every server version accepts:
+
+`generic`, `mysql`, `postgresql`, `sqlite`, `sqlserver`, `oracle`, `snowflake`
+
+Map user wording onto them: Postgres/PG -> `postgresql`; MSSQL/T-SQL/Azure SQL -> `sqlserver`; PL/SQL -> `oracle`. The result is `{valid, error?, dialect?}`.
+
+## CLI-only dialects
+
+Newer server builds also list `mariadb` and `clickhouse`. If the connected server's enum does not include them, use the local CLI:
+
+```sh
+gosqlx validate --dialect mariadb query.sql
+gosqlx validate --dialect clickhouse query.sql
+gosqlx validate --list-dialects # prints every name the CLI accepts
+```
+
+## Steps
+
+1. Validate with the requested dialect. If it fails, also validate with `generic` (or a neighbouring dialect) to tell a real syntax error apart from a dialect-specific construct.
+2. Explain the error at its line/column and name the construct involved (for example backtick vs double-quote identifiers, `LIMIT` vs `TOP`/`FETCH FIRST`, `ILIKE`, `::` casts, `QUALIFY`, `ON DUPLICATE KEY UPDATE` vs `ON CONFLICT`).
+3. Suggest an equivalent for the target dialect. Separate GoSQLX's verdict from your own knowledge of the database.
+4. A pass means GoSQLX parses it; it does not prove the target database accepts every function or type. Say so when it matters.
+
+For conversion, the CLI has `gosqlx transpile --from --to `; it currently supports mysql -> postgres, postgres -> mysql and postgres -> sqlite.
diff --git a/plugins/gosqlx/skills/sql-formatting/SKILL.md b/plugins/gosqlx/skills/sql-formatting/SKILL.md
new file mode 100644
index 00000000..d6b1ffd7
--- /dev/null
+++ b/plugins/gosqlx/skills/sql-formatting/SKILL.md
@@ -0,0 +1,36 @@
+---
+name: sql-formatting
+description: Format or pretty-print SQL with GoSQLX - consistent indentation, keyword casing and optional trailing semicolon. Use when the user asks to format, beautify, pretty-print, reindent or clean up SQL, or to make a .sql file consistent.
+argument-hint: "[SQL string or path to .sql file]"
+allowed-tools: mcp__plugin_gosqlx_gosqlx__format_sql mcp__plugin_gosqlx_gosqlx__validate_sql Read Bash(gosqlx format:*)
+---
+
+# SQL formatting with GoSQLX
+
+Input: `$ARGUMENTS` - a SQL string or a path to a `.sql` file.
+
+## Steps
+
+1. Get the SQL (read the file if a path was given).
+2. Call `format_sql` (`mcp__plugin_gosqlx_gosqlx__format_sql`) with:
+ - `sql` (required)
+ - `indent_size` - spaces per level, default 2
+ - `uppercase_keywords` - default `false` on the MCP tool; set `true` when the user wants `SELECT`/`FROM` style or the existing file uses uppercase keywords
+ - `add_semicolon` - default `false`
+3. If formatting fails, the SQL probably does not parse: call `validate_sql` and report the error instead of guessing a fix.
+4. Show the formatted SQL in a fenced `sql` block. When the input was a file, write the result back only if the user asked you to change the file; otherwise show it.
+
+Formatting is whitespace and keyword-case only - it must not change meaning. If the output appears to drop or alter anything (comments, hints, dialect syntax), say so and keep the original.
+
+## Fallback when the MCP tools are unavailable
+
+```sh
+gosqlx format query.sql # print formatted SQL (keywords uppercased by default)
+gosqlx format --indent 4 query.sql
+gosqlx format --uppercase=false query.sql # keep original keyword case (do not use --no-uppercase: it currently uppercases)
+gosqlx format --check query.sql # exit non-zero if the file needs formatting
+gosqlx format -i query.sql # rewrite in place - only when asked
+echo "select a from b" | gosqlx format
+```
+
+Note the defaults differ: the CLI uppercases keywords by default, the MCP tool does not.
diff --git a/plugins/gosqlx/skills/sql-linting/SKILL.md b/plugins/gosqlx/skills/sql-linting/SKILL.md
new file mode 100644
index 00000000..873751a3
--- /dev/null
+++ b/plugins/gosqlx/skills/sql-linting/SKILL.md
@@ -0,0 +1,73 @@
+---
+name: sql-linting
+description: Lint SQL with GoSQLX and explain its rules L001-L030 (whitespace, keyword case, comma placement, aliasing, plus safety, performance and naming rules), including which rules each entry point applies and which can be auto-fixed. Use when the user asks to lint SQL, enforce SQL style, explains a GoSQLX rule ID like L007 or L019, or sets up SQL linting in CI.
+argument-hint: "[SQL string, .sql file, or rule ID like L016]"
+allowed-tools: mcp__plugin_gosqlx_gosqlx__lint_sql Read Bash(gosqlx lint:*)
+---
+
+# SQL linting with GoSQLX
+
+Input: `$ARGUMENTS` - SQL, a `.sql` file, or a rule ID to explain.
+
+## Which rules run where
+
+| Entry point | Rules applied |
+|---|---|
+| MCP `lint_sql` (`mcp__plugin_gosqlx_gosqlx__lint_sql`) | L001-L010 (style). Keyword case expected UPPER, trailing commas, max line 100 |
+| `gosqlx lint` CLI | L001-L010; `--auto-fix` fixes L001, L002, L003, L007, L010 |
+| `gosqlx.Lint()` Go library | All 30 rules, L001-L030 |
+
+L011-L030 are not returned by `lint_sql` or the CLI. When you spot one of those patterns yourself, cite the rule ID as guidance and say it comes from the Go library's rule set.
+
+## Rules
+
+| ID | Rule | Severity | Auto-fix |
+|---|---|---|---|
+| L001 | Trailing whitespace | Warning | Yes |
+| L002 | Mixed indentation (tabs and spaces) | Error | Yes |
+| L003 | Consecutive blank lines | Warning | Yes |
+| L004 | Indentation depth (excessive nesting) | Warning | No |
+| L005 | Long lines | Info | No |
+| L006 | Column alignment in SELECT lists | Info | No |
+| L007 | Keyword case consistency | Warning | Yes |
+| L008 | Comma placement (trailing vs leading) | Info | No |
+| L009 | Aliasing consistency | Warning | No |
+| L010 | Redundant whitespace | Info | Yes |
+| L011 | DELETE without WHERE | Error | No |
+| L012 | UPDATE without WHERE | Error | No |
+| L013 | DROP without IF EXISTS | Warning | No |
+| L014 | TRUNCATE TABLE | Warning | No |
+| L015 | SELECT ... INTO OUTFILE / DUMPFILE | Error | No |
+| L016 | SELECT * | Warning | No |
+| L017 | Missing WHERE on full scan (no WHERE or LIMIT) | Warning | No |
+| L018 | Leading wildcard LIKE (`'%x'`) | Warning | No |
+| L019 | NOT IN with NULL risk (prefer NOT EXISTS) | Warning | No |
+| L020 | Correlated subquery in SELECT list (N+1) | Warning | No |
+| L021 | OR chain instead of IN | Warning | No |
+| L022 | Function on indexed column in WHERE | Warning | No |
+| L023 | Implicit cross join (comma joins) | Warning | No |
+| L024 | Table alias required | Warning | No |
+| L025 | Reserved keyword used as identifier | Warning | No |
+| L026 | INSERT without explicit column list | Warning | No |
+| L027 | UNION instead of UNION ALL | Warning | No |
+| L028 | LIMIT without ORDER BY | Warning | No |
+| L029 | Subquery that can be a JOIN | Warning | No |
+| L030 | DISTINCT on many columns | Warning | No |
+
+Full reference with examples: https://github.com/ajitpratap0/GoSQLX/blob/main/docs/LINTING_RULES.md
+
+## Steps
+
+1. Call `lint_sql` with `{"sql": "..."}`. Each violation has `rule`, `rule_name`, `severity`, `message`, `line`, `column` and `suggestion`.
+2. Group violations by rule, show the offending line, and give the fix. For auto-fixable rules, mention `gosqlx lint --auto-fix`.
+3. For a rule-ID question, explain the rule from the table above and show a before/after example.
+
+## Fallback when the MCP tools are unavailable
+
+```sh
+gosqlx lint query.sql
+gosqlx lint -r ./queries/ # recurse into a directory
+gosqlx lint --auto-fix query.sql # rewrites the file - only when asked
+gosqlx lint --fail-on-warn query.sql # non-zero exit on warnings (CI)
+gosqlx lint --max-length 120 query.sql
+```
diff --git a/plugins/gosqlx/skills/sql-security-review/SKILL.md b/plugins/gosqlx/skills/sql-security-review/SKILL.md
new file mode 100644
index 00000000..acf0edef
--- /dev/null
+++ b/plugins/gosqlx/skills/sql-security-review/SKILL.md
@@ -0,0 +1,46 @@
+---
+name: sql-security-review
+description: SQL injection review with GoSQLX - scan SQL text for injection patterns (tautologies, UNION attacks, stacked queries, comment bypasses, time-based and out-of-band probes, dangerous functions) and review application code that builds SQL strings (concatenation, f-strings, fmt.Sprintf, template literals), recommending parameterized queries. Use when the user asks whether SQL or database code is safe, about SQL injection, or to security-review queries or data-access code.
+argument-hint: "[SQL string, .sql file, or source file that builds SQL]"
+allowed-tools: mcp__plugin_gosqlx_gosqlx__security_scan mcp__plugin_gosqlx_gosqlx__validate_sql Read Grep Glob Bash(gosqlx analyze:*) Bash(gosqlx lint:*)
+---
+
+# SQL security review with GoSQLX
+
+Input: `$ARGUMENTS` - a SQL string, a `.sql` file, or source code that constructs SQL.
+
+## 1. Scan SQL text
+
+Call `security_scan` (`mcp__plugin_gosqlx_gosqlx__security_scan`) with `{"sql": "..."}`. It returns `is_clean`, per-severity counts (`critical_count`, `high_count`, `medium_count`, `low_count`) and `findings[]`, each with `severity`, `pattern`, `description`, `risk` and `suggestion`. Pattern types: `TAUTOLOGY`, `COMMENT_BYPASS`, `STACKED_QUERY`, `UNION_BASED`, `UNION_INJECTION`, `UNION_GENERIC`, `TIME_BASED`, `BOOLEAN_BASED`, `OUT_OF_BAND`, `DANGEROUS_FUNCTION`.
+
+The scanner inspects a finished SQL string. It catches injected payloads, not the vulnerable code path that lets them in.
+
+## 2. Review code that builds SQL
+
+Use Grep/Read to find where queries are assembled. Flag any user-influenced value that reaches the SQL text through:
+
+- string concatenation or interpolation: `"... WHERE id = " + id`, Python f-strings / `%` / `.format()`, JS template literals, Go `fmt.Sprintf`, Java `String.format`, PHP `"$var"` in query strings;
+- ORM raw-query escape hatches (`raw()`, `extra()`, `Sequelize.literal`, `knex.raw`, `db.Exec(fmt.Sprintf(...))`) fed with user input;
+- dynamic identifiers (table, column, `ORDER BY` direction) taken from input.
+
+For each finding give: file:line, the tainted source, why it is injectable, and a fix:
+
+- **Values** -> placeholders/bind parameters (`?`, `$1`, `:name`, `@p1`) with the driver's parameter API.
+- **Identifiers / sort direction** -> map input onto a hard-coded allowlist; never quote-escape by hand.
+- **LIKE patterns** -> bind the pattern and escape `%`/`_` in user text.
+
+To exercise the scanner on code findings, reconstruct the SQL with a hostile value (for example `' OR 1=1 --`) and run `security_scan` on it.
+
+## 3. Report
+
+Lead with CRITICAL/HIGH issues. Separate GoSQLX scanner findings from your own code-review findings. Do not claim code is safe just because `is_clean` is true for a sample query.
+
+## Fallback when the MCP tools are unavailable
+
+```sh
+gosqlx analyze --security query.sql
+gosqlx lint --security query.sql # adds LIKE-injection, tautology and UNION checks to lint
+echo "SELECT * FROM users WHERE id = 1 OR 1=1" | gosqlx analyze --security
+```
+
+Never run suspected injection payloads against a real database.
From 08bbfc6ec363c8cdccdca7906810ee51401c06c0 Mon Sep 17 00:00:00 2001
From: Ajit Pratap Singh
Date: Tue, 20 Oct 2026 00:42:56 +0530
Subject: [PATCH 3/8] docs: add terms of service, update privacy policy and
integration docs
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
Claude-Session: https://claude.ai/code/session_01L2wnDjnjXs2u8baaf2PC6w
---
CHANGELOG.md | 17 +++
README.md | 4 +
docs/MCP_GUIDE.md | 26 +++-
website/src/app/privacy/page.tsx | 46 ++++++-
website/src/app/sitemap.ts | 1 +
website/src/app/terms/page.tsx | 162 +++++++++++++++++++++++
website/src/components/layout/Footer.tsx | 1 +
7 files changed, 249 insertions(+), 8 deletions(-)
create mode 100644 website/src/app/terms/page.tsx
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f33b1f40..8cf0b59c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -212,6 +212,23 @@ acceptance, CLI refusals), so read **Behavior changes** before upgrading.
`WINDOW w AS (w2)`. (#608, #629)
### Added
+- **Claude Code plugin and marketplace**: `/plugin marketplace add
+ ajitpratap0/GoSQLX` then `/plugin install gosqlx@gosqlx` installs the
+ `gosqlx` plugin (`plugins/gosqlx/`): the hosted MCP server, five SQL skills
+ (analysis, formatting, security review, linting, dialects), four slash
+ commands, a read-only `sql-reviewer` agent and an opt-out PostToolUse hook
+ that validates edited `.sql` files when the `gosqlx` CLI is installed.
+- **ChatGPT app support in `gosqlx-mcp`** (OpenAI Apps SDK and MCP Apps):
+ `analyze_sql` links an embedded report widget
+ (`ui://gosqlx/analysis-v1.html`, `text/html;profile=mcp-app`); all tools now
+ return `structuredContent` alongside the unchanged text JSON, carry
+ `securitySchemes`/invocation-status metadata, and the server sends usage
+ instructions. `validate_sql` accepts `mariadb` and `clickhouse`; the server
+ reports `gosqlx.Version`; `/health` reports the real tool count; the
+ optional `GOSQLX_MCP_OPENAI_APPS_CHALLENGE` env var serves
+ `/.well-known/openai-apps-challenge` for app verification; idle MCP sessions
+ are swept after 30 minutes instead of living until restart. Setup:
+ `integrations/chatgpt/README.md`, details: `docs/CHATGPT_APP.md`.
- **`--dialect` flag for the `parse`, `format`, `analyze`, and `lint` CLI
commands**: dialect-specific SQL (e.g. MySQL `LIMIT offset, count`) can now be
parsed by these commands, matching the existing `validate --dialect`. Unknown
diff --git a/README.md b/README.md
index 2f13a5f0..3a7fe52c 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,10 @@ claude mcp add --transport http gosqlx https://mcp.gosqlx.dev/mcp
7 SQL tools in Claude, Cursor, or any MCP client. [MCP guide](https://gosqlx.dev/docs/integrations/mcp/).
+**Claude Code plugin** (skills, slash commands, SQL reviewer agent): `/plugin marketplace add ajitpratap0/GoSQLX` then `/plugin install gosqlx@gosqlx` - [details](plugins/gosqlx/README.md).
+
+**ChatGPT app**: add `https://mcp.gosqlx.dev/mcp` as a connector in developer mode for an interactive analysis widget - [details](integrations/chatgpt/README.md).
+
[](https://mcp.gosqlx.dev/health)
[](https://glama.ai/mcp/servers/ajitpratap0/GoSQLX)
diff --git a/docs/MCP_GUIDE.md b/docs/MCP_GUIDE.md
index 04dec099..045a3183 100644
--- a/docs/MCP_GUIDE.md
+++ b/docs/MCP_GUIDE.md
@@ -595,6 +595,28 @@ curl -s -X POST http://127.0.0.1:8080/mcp \
## AI Assistant Integration
+### Claude Code plugin
+
+The repository doubles as a Claude Code plugin marketplace. The `gosqlx` plugin
+connects to the hosted server and adds SQL skills, `/gosqlx:analyze`,
+`/gosqlx:format`, `/gosqlx:lint` and `/gosqlx:security` commands, a read-only
+`sql-reviewer` agent and an optional hook that validates edited `.sql` files:
+
+```
+/plugin marketplace add ajitpratap0/GoSQLX
+/plugin install gosqlx@gosqlx
+```
+
+See [`plugins/gosqlx/README.md`](../plugins/gosqlx/README.md) for local mode and privacy notes.
+
+### ChatGPT
+
+`gosqlx-mcp` is also a ChatGPT app (OpenAI Apps SDK): `analyze_sql` renders an
+interactive report widget, and every tool returns `structuredContent`. Add
+`https://mcp.gosqlx.dev/mcp` as a connector in ChatGPT developer mode (no auth).
+See [`integrations/chatgpt/README.md`](../integrations/chatgpt/README.md) and
+[CHATGPT_APP.md](CHATGPT_APP.md).
+
### Claude Desktop
Add `gosqlx-mcp` to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
@@ -604,9 +626,7 @@ Add `gosqlx-mcp` to your Claude Desktop configuration (`~/Library/Application Su
"mcpServers": {
"gosqlx": {
"command": "gosqlx-mcp",
- "env": {
- "GOSQLX_MCP_PORT": "8080"
- }
+ "args": ["--stdio"]
}
}
}
diff --git a/website/src/app/privacy/page.tsx b/website/src/app/privacy/page.tsx
index 409c10e3..1ffa24ab 100644
--- a/website/src/app/privacy/page.tsx
+++ b/website/src/app/privacy/page.tsx
@@ -5,7 +5,7 @@ import { pageJsonLd } from '@/lib/jsonld';
export const metadata: Metadata = pageMeta({
title: 'Privacy Policy',
- description: 'GoSQLX privacy policy: no tracking, no telemetry and no query logging. What the website and the playground do and do not collect.',
+ description: 'GoSQLX privacy policy: no tracking, no telemetry and no query logging. What the website, playground, hosted MCP server, ChatGPT app and Claude plugin do and do not collect.',
path: '/privacy/',
image: { url: '/opengraph-image', alt: 'GoSQLX' },
});
@@ -29,6 +29,10 @@ export default function PrivacyPage() {
(and is not) collected across our website, tools, and services.
+
+ Last updated: October 20, 2026
+
+
Website
The GoSQLX website is a static site. We use Vercel Speed Insights, a
@@ -47,11 +51,36 @@ export default function PrivacyPage() {
ever sent to our servers.
- Remote MCP Server
+ Remote MCP Server, ChatGPT App and Claude Plugin
+
+ The hosted MCP server at mcp.gosqlx.dev also powers the GoSQLX
+ ChatGPT app and, by default, the GoSQLX Claude plugin. When an AI
+ assistant calls a GoSQLX tool, the server receives the tool
+ arguments: the SQL text and options such as the dialect or
+ indentation. It analyzes them in memory, returns the result and
+ keeps nothing. It does not log, store, sell or share SQL text or
+ results, and it does not use them to train models.
+
+
+ To serve requests, the server holds short-lived, in-memory state
+ that is never written to disk: an MCP session identifier for each
+ connected client, cleared after 30 minutes of inactivity, and per-IP
+ rate-limit counters, cleared after 10 minutes of inactivity. Request metadata that ChatGPT may attach, such as
+ locale, user agent or approximate location, is ignored.
+
+
+ The server is hosted on Render behind Cloudflare. As our network and
+ hosting providers, they process connection data such as IP
+ addresses and request times to deliver and protect the service,
+ under their own privacy policies. Your conversations with ChatGPT or
+ Claude are handled by OpenAI or Anthropic under their policies; we
+ never see them, only the tool calls the assistant makes.
+
- The hosted MCP server is stateless. It does not log, store, or
- transmit any SQL queries it processes. IP-based rate-limit counters
- are held in memory and automatically cleared after 10 minutes.
+ You stay in control: the plugin can run against a local server, and
+ the open-source server can be self-hosted so that SQL never leaves
+ your infrastructure. Do not send secrets or personal data in SQL to
+ any hosted service.
VS Code Extension
@@ -68,6 +97,13 @@ export default function PrivacyPage() {
review them independently.
+ Changes
+
+ We will update this page when our data handling changes; the date
+ above shows the latest revision. See also our{' '}
+ Terms of Service.
+
+
Contact
If you have questions about this privacy policy, please open an issue
diff --git a/website/src/app/sitemap.ts b/website/src/app/sitemap.ts
index b61e75e1..36f01db5 100644
--- a/website/src/app/sitemap.ts
+++ b/website/src/app/sitemap.ts
@@ -32,6 +32,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
'/benchmarks/',
'/compare/',
'/privacy/',
+ '/terms/',
]
.concat(COMPETITOR_PAGES.map((c) => `/compare/${c.slug}/`))
.map((p) => ({ url: `${baseUrl}${p}` }));
diff --git a/website/src/app/terms/page.tsx b/website/src/app/terms/page.tsx
new file mode 100644
index 00000000..f91e1ed8
--- /dev/null
+++ b/website/src/app/terms/page.tsx
@@ -0,0 +1,162 @@
+import type { Metadata } from 'next';
+import { pageMeta } from '@/lib/seo';
+import { JsonLd } from '@/components/JsonLd';
+import { pageJsonLd } from '@/lib/jsonld';
+
+export const metadata: Metadata = pageMeta({
+ title: 'Terms of Service',
+ description: 'Terms for using the GoSQLX website, playground, hosted MCP server, ChatGPT app and Claude plugin: free, as-is, read-only SQL analysis with fair-use limits.',
+ path: '/terms/',
+ image: { url: '/opengraph-image', alt: 'GoSQLX' },
+});
+
+const jsonLd = pageJsonLd({
+ name: 'Terms of Service',
+ description: metadata.description as string,
+ path: '/terms/',
+ crumbs: [{ name: 'Terms of Service', path: '/terms/' }],
+});
+
+export default function TermsPage() {
+ return (
+ <>
+
+
+
+
Terms of Service
+
+ These terms cover the GoSQLX hosted services: the gosqlx.dev website
+ and playground, the remote MCP server at mcp.gosqlx.dev, the GoSQLX
+ ChatGPT app and the GoSQLX Claude plugin (together, the
+ “Services”). By using the Services you agree to these
+ terms.
+
+
+ Last updated: October 20, 2026
+
+
+
The software
+
+ The GoSQLX source code, CLI, libraries, VS Code extension and MCP
+ server binary are open source under the{' '}
+
+ Apache License 2.0
+
+ . That license, not these terms, governs your use, modification and
+ distribution of the software, including when you run it yourself.
+
+
+
What the Services do
+
+ The Services parse, validate, format, lint and scan SQL text that you
+ (or an AI assistant acting for you) send to them, and return the
+ analysis. They never connect to a database and never execute SQL.
+ The Services are free and require no account.
+
+
+
Acceptable use
+
You agree not to:
+
+ -
+ send secrets, credentials, or personal or confidential data in SQL
+ you submit to a hosted Service (self-host the open-source server
+ for sensitive SQL);
+
+ -
+ attempt to overload, disrupt, probe for vulnerabilities in or get
+ around the rate limits of the Services, or access them by automated
+ means at a volume beyond normal interactive or CI use;
+
+ -
+ use the Services to break the law or to infringe anyone’s
+ rights.
+
+
+
+ We may rate-limit, block or stop serving traffic that breaks these
+ rules or threatens the availability of the Services for others.
+ Report security issues privately as described in our{' '}
+
+ security policy
+
+ .
+
+
+
Your content
+
+ You keep all rights to the SQL you submit. You grant us only the
+ permission needed to process it and return a result. How the hosted
+ Services handle your data is described in our{' '}
+ Privacy Policy.
+
+
+
AI assistants
+
+ When you use GoSQLX through ChatGPT, Claude or another AI assistant,
+ your use of that assistant is governed by its provider’s own
+ terms and privacy policy. The assistant decides when to call GoSQLX
+ tools and what to tell you about the results; we are not responsible
+ for the assistant’s output.
+
+
+
No warranty
+
+ The Services are provided “as is” and “as
+ available”, without warranties of any kind, express or implied,
+ including merchantability, fitness for a particular purpose and
+ non-infringement. Analysis results, including security findings and
+ lint suggestions, may be incomplete or wrong. A clean security scan
+ does not mean SQL is safe. Review results before you rely on them.
+
+
+
Availability and changes
+
+ The hosted Services are run on a best-effort basis with no uptime
+ commitment. We may change, suspend or discontinue any of them at any
+ time. We may update these terms; the date above shows the latest
+ revision, and continued use after a change means you accept it.
+
+
+
Limitation of liability
+
+ To the maximum extent permitted by law, the GoSQLX authors and
+ maintainers are not liable for any indirect, incidental, special,
+ consequential or punitive damages, or for any loss of data, profits
+ or business, arising from your use of or inability to use the
+ Services.
+
+
+
Contact and support
+
+ For questions about these terms or help with the Services, open an
+ issue on{' '}
+
+ GitHub Issues
+ {' '}
+ or start a thread in{' '}
+
+ GitHub Discussions
+
+ .
+
+
+
+ >
+ );
+}
diff --git a/website/src/components/layout/Footer.tsx b/website/src/components/layout/Footer.tsx
index d13eb8d5..52e0e0a2 100644
--- a/website/src/components/layout/Footer.tsx
+++ b/website/src/components/layout/Footer.tsx
@@ -19,6 +19,7 @@ const FOOTER_LINKS = {
{ label: 'Changelog', href: '/changelog' },
{ label: 'CHANGELOG.md on GitHub', href: 'https://github.com/ajitpratap0/GoSQLX/blob/main/CHANGELOG.md', external: true },
{ label: 'Privacy Policy', href: '/privacy' },
+ { label: 'Terms of Service', href: '/terms' },
],
Community: [
{ label: 'GitHub', href: 'https://github.com/ajitpratap0/GoSQLX', external: true },
From 13645174223a0daeb699f9239bbd5c45f45422e4 Mon Sep 17 00:00:00 2001
From: Ajit Pratap Singh
Date: Tue, 20 Oct 2026 01:14:25 +0530
Subject: [PATCH 4/8] docs(legal): harden terms and privacy policy for app
directory review
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
Claude-Session: https://claude.ai/code/session_01L2wnDjnjXs2u8baaf2PC6w
---
integrations/chatgpt/README.md | 4 +-
vscode-extension/package.json | 2 +-
vscode-extension/src/utils/telemetry.ts | 6 +-
website/src/app/privacy/page.tsx | 207 ++++++++++++++++++------
website/src/app/terms/page.tsx | 157 +++++++++++++-----
5 files changed, 284 insertions(+), 92 deletions(-)
diff --git a/integrations/chatgpt/README.md b/integrations/chatgpt/README.md
index 8c85ce93..2f4130ef 100644
--- a/integrations/chatgpt/README.md
+++ b/integrations/chatgpt/README.md
@@ -139,8 +139,8 @@ available to projects with EU data residency.
- [ ] Privacy policy URL: . It must cover the data
categories, purposes, recipients and user controls.
- [ ] Terms of service URL: .
-- [ ] Support contact: (if the form
- insists on an email address, use a maintainer-controlled inbox).
+- [ ] Support contact: ; email
+ `privacy@gosqlx.dev` (forwarding must be set up on gosqlx.dev).
- [ ] Logo (light and dark), at least 3 screenshots of the `analyze_sql` widget, and a
short demo video.
- [ ] Test cases: use the positive and negative example prompts above
diff --git a/vscode-extension/package.json b/vscode-extension/package.json
index ffe408f5..09f3ec3f 100644
--- a/vscode-extension/package.json
+++ b/vscode-extension/package.json
@@ -151,7 +151,7 @@
"gosqlx.telemetry.enable": {
"type": "boolean",
"default": false,
- "description": "Enable anonymous usage telemetry to help improve GoSQLX. No SQL content or file paths are collected."
+ "description": "Record anonymous usage statistics in memory on this machine. They are never transmitted. No SQL content or file paths are collected."
},
"gosqlx.performance.showStatusBar": {
"type": "boolean",
diff --git a/vscode-extension/src/utils/telemetry.ts b/vscode-extension/src/utils/telemetry.ts
index fc8472ac..ad6650b8 100644
--- a/vscode-extension/src/utils/telemetry.ts
+++ b/vscode-extension/src/utils/telemetry.ts
@@ -314,8 +314,8 @@ export class TelemetryManager {
* Prompts user to opt-in to telemetry.
*/
export async function promptTelemetryOptIn(): Promise {
- const message = 'Would you like to help improve GoSQLX by sharing anonymous usage data? ' +
- 'No SQL content or file paths are ever collected.';
+ const message = 'Allow GoSQLX to record anonymous usage statistics on this machine? ' +
+ 'They are never transmitted, and no SQL content or file paths are collected.';
const learnMore = 'Learn More';
const yes = 'Yes, help improve';
@@ -330,7 +330,7 @@ export async function promptTelemetryOptIn(): Promise {
if (choice === learnMore) {
vscode.env.openExternal(
- vscode.Uri.parse('https://github.com/ajitpratap0/GoSQLX/blob/main/PRIVACY.md')
+ vscode.Uri.parse('https://gosqlx.dev/privacy/#vscode')
);
// Re-prompt after they've had a chance to read
return promptTelemetryOptIn();
diff --git a/website/src/app/privacy/page.tsx b/website/src/app/privacy/page.tsx
index 1ffa24ab..2dd35e64 100644
--- a/website/src/app/privacy/page.tsx
+++ b/website/src/app/privacy/page.tsx
@@ -5,7 +5,7 @@ import { pageJsonLd } from '@/lib/jsonld';
export const metadata: Metadata = pageMeta({
title: 'Privacy Policy',
- description: 'GoSQLX privacy policy: no tracking, no telemetry and no query logging. What the website, playground, hosted MCP server, ChatGPT app and Claude plugin do and do not collect.',
+ description: 'GoSQLX privacy policy: no tracking cookies and no query logging. What the website, playground, hosted MCP server, ChatGPT app, Claude plugin and VS Code extension collect, why, who receives it and your rights.',
path: '/privacy/',
image: { url: '/opengraph-image', alt: 'GoSQLX' },
});
@@ -17,6 +17,8 @@ const jsonLd = pageJsonLd({
crumbs: [{ name: 'Privacy Policy', path: '/privacy/' }],
});
+const CONTACT_EMAIL = 'privacy@gosqlx.dev';
+
export default function PrivacyPage() {
return (
<>
@@ -25,91 +27,202 @@ export default function PrivacyPage() {