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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions