Skip to content

Expose ClickHouse-compatible HTTP API alongside MCP/OpenAPI #87

@BorisTyshkevich

Description

@BorisTyshkevich

Problem

altinity-mcp with --openapi already looks more like a ClickHouse frontend than a pure MCP server. To make it a drop-in plug-in replacement in front of ClickHouse, it should also speak the native ClickHouse HTTP API documented at https://clickhouse.com/docs/interfaces/http, so existing tools, dashboards, and clients can point at altinity-mcp without changes:

curl 'http://mcp.example.com/?query=SELECT+1'
curl -u alice:pass 'http://mcp.example.com/?query=SELECT+1&database=analytics&default_format=JSONCompact'
curl -X POST 'http://mcp.example.com/' --data-binary 'SELECT 1'

Today any of these hit the MCP streamable-HTTP endpoint and fail.

Feasibility — hook points already exist

The HTTP router is centralized:

  • cmd/altinity-mcp/main.go — mux setup around mcpRoutePatterns (MCP transport) and openAPIRoutePatterns (lines 407-433); a new chHTTPRoutePatterns can be added to the same mux or, better, registered as a catch-all that sniffs ?query= or a POST body before handing off to MCP.
  • pkg/server/server.go:2519 — the OpenAPI dispatcher already routes by path suffix in a switch. Adding a case that matches ?query= is a one-line addition.
  • handleExecuteQueryOpenAPI (pkg/server/server.go) — the core 'parse request → run query → serialize' pipeline already exists. The CH-compat handler can reuse it; the work is input parsing (CH params/headers/body) and output formatting (TSV/JSON-variants), not query execution.
  • GetClickHouseClientFromCtx / GetClickHouseClientWithOAuth — already resolves the right backend connection based on JWE/OAuth/cluster-secret context. The CH-compat handler should funnel through the same path so all three auth modes work identically to MCP/OpenAPI.

Proposed dispatch

In the HTTP mux, before the MCP handler, route to a new handleClickHouseHTTP when any of these is true on the request:

  • Method is GET and ?query= is non-empty
  • Method is POST and Content-Type is text/plain; charset=UTF-8 (CH's native format) or the body starts with SQL keywords
  • Request has X-ClickHouse-User / X-ClickHouse-Key / X-ClickHouse-Database headers (unambiguous CH-client signal)

Otherwise fall through to existing MCP/OpenAPI handlers. This keeps MCP and OpenAPI clients unaffected.

Scope — phase 1 (minimum viable plug-in)

Request parsing

  • ?query= (URL-encoded SQL)
  • ?database= → overrides default database
  • ?default_format= → picks response format
  • POST body as query (when URL has no ?query=)
  • Mixed mode: ?query=SELECT+1+FROM+ + POST body = concatenation (matches CH semantics)
  • HTTP Basic Auth (username:password) → maps to ClickHouse user via cluster-secret / static / JWE / OAuth mode (whichever altinity-mcp is configured for)
  • X-ClickHouse-User / X-ClickHouse-Key / X-ClickHouse-Database headers (higher priority than basic auth, per CH docs)

Response formats (pick the most useful subset first)

  • TabSeparated (CH default — required)
  • TabSeparatedWithNames
  • JSONCompact
  • JSONEachRow
  • FORMAT clause detection in the SQL (CH-compat: trailing FORMAT JSONCompact wins over default_format)

Errors

  • Query errors → HTTP 500 with CH-style plain-text body (Code: 62. DB::Exception: ... (SYNTAX_ERROR))
  • Auth failures → HTTP 401 with WWW-Authenticate: Basic header
  • Unknown user / permission denied → HTTP 403

Preserves

  • Read-only mode flag still enforced
  • clickhouse-limit still applied (or: bypass since a real CH client expects unbounded results?)
  • Cluster-secret impersonation still uses oauthClaims.Email if the request arrives authenticated via OAuth

Scope — phase 2 (nice-to-have, file separate issues as needed)

  • Streaming responses for large result sets (currently we buffer everything in memory in executeSelect)
  • INSERT ... FORMAT with body data (POST to upload TSV/CSV)
  • Additional formats: RowBinary, Native, Values, Pretty*, CSV, CSVWithNames
  • ?param_<name>= query parameters (CH's {name:Type} substitution)
  • Compression (Accept-Encoding: gzip, lz4)
  • X-ClickHouse-Progress, X-ClickHouse-Summary response headers
  • readonly, max_result_rows, max_execution_time and other settings as query params

Out of scope

  • Binary native-protocol HTTP upgrades
  • Replica / cluster discovery endpoints (/replicas_status, /ping etc. — these are trivial to add if asked, but not part of 'query execution' plug-in)
  • TLS termination differences — inherit from existing server TLS

Why this is cleaner than it sounds

The CH HTTP API is substantial but altinity-mcp already has:

  1. Query execution with auth context — handleExecuteQueryOpenAPI path
  2. Per-request ClickHouse client resolution with JWE/OAuth/cluster-secret context
  3. QueryResult { Columns, Types, Rows, Count } internal type — straightforward to serialize to TSV/JSON variants

Phase 1 is mostly a format writer + a request parser + one route case. The heavy lifting (auth, execution, OpenAPI parity) stays untouched.

Acceptance criteria

  • curl 'http://localhost:8080/?query=SELECT+1' returns 1\n with Content-Type: text/tab-separated-values
  • curl -u user:pass 'http://localhost:8080/?query=SELECT+currentUser()' runs as user (cluster-secret / JWE / OAuth mode, whichever is configured)
  • curl 'http://localhost:8080/?query=SELECT+1&default_format=JSONCompact' returns valid JSONCompact
  • Invalid SQL returns HTTP 500 with Code: prefix body
  • Existing MCP clients and /openapi endpoints continue to work unchanged
  • Integration test: point a clickhouse-client --url at altinity-mcp and execute a simple SELECT

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions