Meridian
The open-source Trino control plane and MCP server for Kubernetes.
getmeridian.dev · Tutorial · Latest release: v1.5.1
Trino clusters take 30–90 seconds to provision. Meridian eliminates that wait: a hot standby pool keeps pre-provisioned clusters idle and ready, so a reservation completes in zero seconds. No cold starts for CI/CD jobs, batch workloads, or multi-tenant platforms.
Meridian has two parts:
- A Kubernetes operator that manages the full cluster lifecycle (
Empty → Pending → Idle → Reserved) and keeps a warm pool of clusters. - An MCP server that lets Claude, Cursor, or any MCP client explore catalogs, run queries, and reserve clusters in natural language. It is secure by default: read-only queries, a token required for remote access, and config-changing tools off unless you enable them.
Quick Start (Helm)
Installs the operator, its CRDs, and RBAC. Needs Kubernetes 1.26+ and Helm 3.
helm install meridian oci://ghcr.io/meridian-io/charts/meridian --version 1.5.1 \
--namespace meridian --create-namespace
# or from a checkout of this repo
helm install meridian charts/meridian --namespace meridian --create-namespace
Create a pool of two hot-standby Trino clusters and watch them become Idle:
cat <<EOF | kubectl apply -n meridian -f -
apiVersion: meridian.io/v1alpha1
kind: ClusterPool
metadata:
name: demo-pool
spec:
replicas: 2
template:
profile: default
image: trinodb/trino:435
workers: 1
EOF
kubectl -n meridian get clusters -w
Clusters with the default profile include the read-only tpch sample catalog, so a first query works right away:
CLUSTER=$(kubectl -n meridian get clusters -o jsonpath='{.items[0].metadata.name}')
kubectl -n meridian exec deploy/$CLUSTER-coordinator -- trino --execute "SELECT name FROM tpch.tiny.nation LIMIT 5"
Optional parts are off by default:
To run the MCP server in the cluster:
kubectl -n meridian create secret generic meridian-mcp-token --from-literal=token=$(openssl rand -hex 32)
helm upgrade meridian oci://ghcr.io/meridian-io/charts/meridian --version 1.5.1 -n meridian \
--reuse-values --set mcp.enabled=true --set mcp.auth.existingSecret=meridian-mcp-token
The operator reconciles Meridian resources in every namespace, so install the chart once per Kubernetes cluster. The REST certificate and the MCP token are read at startup; restart the pod after rotating them.
All pods run as non-root with a read-only root filesystem and no Linux capabilities. See charts/meridian/values.yaml for every setting.
Upgrading: Helm installs CRDs once and never upgrades them. Before upgrading the chart, apply the new CRDs from the target version:
helm pull oci://ghcr.io/meridian-io/charts/meridian --version <version> --untar
kubectl apply --server-side -f meridian/crds/
Monitoring
The operator and the MCP server export Prometheus metrics. The operator serves them on :8081 and the MCP server on :9090, each behind its own ClusterIP Service (<release>-meridian-operator-metrics, <release>-meridian-mcp-metrics). The endpoints have no token, like most exporters, so keep them inside the cluster. metrics.enabled=false turns off the MCP endpoint and removes the Services.
With the Prometheus Operator and a Grafana that loads dashboards from ConfigMaps:
helm upgrade meridian oci://ghcr.io/meridian-io/charts/meridian -n meridian --reuse-values \
--set metrics.serviceMonitor.enabled=true --set metrics.serviceMonitor.labels.release=<your-prometheus-release> \
--set metrics.dashboard.enabled=true
Without the sidecar, import charts/meridian/dashboards/meridian.json in Grafana.
The meridian_clusters and meridian_pool_desired_replicas gauges are reported by every operator replica, so aggregate them with max by, as the dashboard does. Labels never contain cluster names, client IDs or SQL.
MCP Server
The meridian binary is an MCP server for Trino clusters managed by Meridian. It needs the Meridian CRDs in your Kubernetes cluster. To use an existing Trino deployment, register it as a Cluster resource. Local Development shows how.
Install
Binary (macOS arm64 shown; Linux and Windows builds are on the releases page):
curl -Lo meridian.tar.gz https://github.com/meridian-io/meridian/releases/download/v1.5.1/trino-mcp-server_1.5.1_darwin_arm64.tar.gz
tar xzf meridian.tar.gz && sudo mv meridian /usr/local/bin/
Docker:
docker pull ghcr.io/meridian-io/trino-mcp-server:v1.5.1
Run
# stdio: for Claude Desktop and other local MCP clients
meridian --transport stdio --namespace meridian
# HTTP: for remote or team use, at http://<host>:8080/mcp. A bearer token is required.
export MERIDIAN_MCP_TOKEN=$(openssl rand -hex 32)
meridian --transport http --addr :8080 --namespace meridian
# In-cluster: uses the pod's service account; token from a mounted Secret
meridian --transport http --addr :8080 --auth-token-file /etc/meridian/token
Remote mode follows the current MCP spec (2026-07-28): Streamable HTTP at /mcp, stateless, so any number of replicas can sit behind one load balancer. Older clients (MCP 2025 revisions) work on the same endpoint. The old HTTP+SSE transport (/sse, 2024-11-05) was removed in v1.5; clients that only speak it need an update.
Connect to Claude Code
claude mcp add --transport http meridian https://meridian.example.com/mcp \
--header "Authorization: Bearer $MERIDIAN_MCP_TOKEN"
Connect to Claude Desktop
Add to claude_desktop_config.json. On macOS it's in ~/Library/Application Support/Claude/, and on Windows in %APPDATA%\Claude\:
{
"mcpServers": {
"meridian": {
"command": "meridian",
"args": ["--transport", "stdio", "--namespace", "meridian"]
}
}
}
Security Defaults
Each default can be relaxed with a flag:
The read-only check is a guardrail for AI clients. Trino access control remains the real security boundary. For example, connector table functions such as system.query(...) send SQL to the underlying database and are not inspected by Meridian.
Other flags: --metrics-addr (Prometheus metrics, default :9090 in http mode, off in stdio mode unless set; see Monitoring), --disable-localhost-protection (only for a reverse proxy on the same host that forwards over localhost), --cache-max-entries (default 1000), and --log-format json|text. Tool failures (bad arguments, missing clusters) come back as tool results with isError: true, so the AI client can see and correct them. In HTTP mode, /healthz reports the process is alive and /readyz reports the Kubernetes API is reachable. Neither requires a token.
Tools
Core tools (on by default)
Held tools (enable with --enable-tools)
Query Result Caching
Results are cached in memory to avoid repeated round-trips to Trino:
- Metadata tools (
list_catalogs,list_schemas,list_tables,get_table_schema,explain_query) cache for 5 minutes. execute_queryis not cached by default, so data changes are visible immediately. Passttl=Nto cache static or reference data for N seconds.- Cache hits are marked
[cache hit]. - Catalog changes clear the cache. After
add_catalog,remove_catalogor a credential rotation, every MCP replica drops that cluster's cached results, solist_catalogsreflects the change right away. - Identical concurrent queries run once (singleflight). The second caller gets the first caller's result.
- Cache key: the cluster,
max_rows, and the SQL. A result cut off atmax_rowsis never served to a request that asked for more rows. - Size: at most
--cache-max-entriesresults (default 1000).
Query Tagging
Every statement sent to Trino is tagged with a unique mcp_query_id, as a SQL comment:
SELECT * FROM mysql_testdb.testdb.orders
-- mcp_query_id=3f2a1b4c-e8d7-4a9b-b1c2-0f5e6d7a8b9c
The query_id is shown at the top of every result. Use it to find the exact execution in the Trino UI (http://<coordinator>:8080/ui) or query history. Cache hits don't reach Trino, so they aren't tagged.
CSV Export
For large results, pass output_file with a plain file name. The rows are written as CSV into --output-dir, and only a summary goes back to the AI client:
Run SELECT * FROM mysql_testdb.testdb.orders on trino-local with output_file=orders.csv
Results written to /home/you/.cache/meridian/exports/orders.csv (1042 rows, 8 columns)
query_id: 3f2a1b4c-...
Directories and .. are rejected, and CSV export is disabled in HTTP mode.
Kubernetes Operator
The operator runs three controllers in one binary:
Cluster Lifecycle
Empty → Pending → Idle ⇄ Reserved
│ │
▼ ▼
Failed Degraded (the pool replaces both)
Architecture
┌──────────────────────────────────────────────────────┐
│ Meridian │
│ │
│ ┌─────────────┐ ┌───────────────┐ │
│ │ REST API │ │ MCP Server │ │
│ │ Go / mTLS │ │ Go │ │
│ └──────┬──────┘ └──────┬────────┘ │
│ │ │ │
│ ┌──────▼────────────────────────▼──────┐ │
│ │ Kubernetes API Server │ │
│ └──────┬───────────────────────────────┘ │
│ ┌─────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ Cluster ClusterPool ClusterPool │
│ Controller Controller Autoscaler │
└──────────────────────────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Cluster(Idle) Cluster(Reserved) Cluster(Pending)
CRDs (meridian.io/v1alpha1)
Pod Sizing (Profiles)
Every Trino pod gets CPU and memory from its cluster's profile. The memory request equals the limit, so pods aren't OOM-killed by overcommit. CPU has a request but no limit, because CPU limits throttle the JVM.
Custom profiles live in a ConfigMap named meridian-profile-<name>. All keys are optional:
apiVersion: v1
kind: ConfigMap
metadata:
name: meridian-profile-analytics
namespace: meridian
data:
jvmHeap: "24G" # pod memory defaults to 125% of the heap if not set
queryMaxMemory: "48GB"
queryMaxMemoryPerNode: "6GB"
memory: "30Gi" # both roles
cpu: "6"
coordinatorMemory: "16Gi" # role-specific overrides
workerCPU: "8"
sampleCatalogs: "" # tpch, tpcds, or "" for none
Sample catalogs. tpch and tpcds are read-only generated datasets bundled with Trino. They need no credentials or external systems. The default profile includes tpch, and a custom profile inherits it unless it sets sampleCatalogs. medium and large include none. They're written into each pod's configuration at startup, so they survive pod restarts.
Sizes and sample catalogs apply to newly created clusters. Existing clusters pick them up when the pool replaces them (image upgrade or maxClusterAge).
Operator Flags
Run the Operator
Use the Helm chart for clusters. To run from source against your current kubeconfig:
make install # apply CRDs
make build # → bin/meridian-operator
./bin/meridian-operator --namespace meridian
# with the mTLS REST API
./bin/meridian-operator --namespace meridian \
--rest-addr :8443 --tls-cert cert.pem --tls-key key.pem --tls-ca ca.pem
--tls-ca turns on client-certificate checks, which the API needs: the client's identity comes from its certificate. Without it the API can't identify clients, answers reservation requests with 401, and logs a warning. If the CA file can't be read, the operator refuses to start.
Reserve a Cluster (REST API)
The client identity comes from the mTLS client certificate's CN.
curl -X POST https://meridian:8443/api/v1/clusters/reservations \
--cert client.crt --key client.key \
-H "Content-Type: application/json" \
-d '{"reservationId": "job-123", "profile": "default"}'
{"clusterName": "pool-abc123", "coordinatorUrl": "http://pool-abc123-coordinator.meridian.svc.cluster.local:8080", "reservedAt": "2026-03-01T10:00:00Z"}
A reservation is identified by the client and its reservationId, so sending the same request again returns the same cluster. If no cluster is idle, the request waits up to 30 seconds and then returns 503. Other failures return 500.
Release it with DELETE /api/v1/clusters/reservations/job-123: 204 when released, 403 if another client holds that ID, 404 if there is no such reservation, and 409 if the cluster changed during the release (send it again).
Local Development
This setup runs Trino and MySQL in Docker and a local kind Kubernetes cluster, so you can test every tool end to end without cloud infrastructure.
Prerequisites: Docker Desktop, Go 1.26+, then brew install kind kubectl helm.
1. Start Trino and MySQL
cd hack && docker compose up -d
Wait until Trino is ready:
curl -sf http://localhost:8080/v1/info | python3 -c "import sys,json; print('ready' if not json.load(sys.stdin).get('starting') else 'starting')"
2. Create a Local Kubernetes Cluster
./hack/setup-local.sh
This creates a kind cluster named meridian-dev, applies the CRDs, seeds test clusters, and registers the Docker Trino as a Cluster named trino-local (coordinator http://localhost:8080).
3. Build and Connect the MCP Server
make build-mcp # → bin/meridian
Point Claude Desktop at the local build. The catalog tools are enabled here because the walkthrough below adds a catalog:
{
"mcpServers": {
"meridian": {
"command": "/path/to/meridian/bin/meridian",
"args": ["--transport", "stdio", "--namespace", "meridian",
"--kubeconfig", "/Users/you/.kube/config",
"--enable-tools", "add_catalog,remove_catalog"]
}
}
}
Restart Claude Desktop to load the tools.
Note: When adding the MySQL catalog, use
mysql:3306in the connection URL, notlocalhost:3306. Trino runs inside Docker and reaches MySQL by its Docker network hostname.
4. Build and Test
make build # operator → bin/meridian-operator
make build-mcp # MCP server → bin/meridian
make test-all # all tests (operator + MCP)
make manifests # regenerate CRDs and RBAC after changing API types (also copies them into the chart)
make helm-lint # lint the chart and validate it against Kubernetes schemas (needs kubeconform)
To try the chart on the kind cluster with locally built images:
make docker-build docker-build-mcp
kind load docker-image --name meridian-dev ghcr.io/meridian-io/trino-operator:dev ghcr.io/meridian-io/trino-mcp-server:dev
helm install meridian charts/meridian -n meridian \
--set operator.image.tag=dev --set operator.image.pullPolicy=Never
CI runs build, vet, race tests, golangci-lint, and govulncheck for both modules, plus gofmt, a check that generated CRDs and RBAC match the Go types, and the chart lint.
5. End-to-End Tests
make e2e runs Meridian on a new kind cluster (meridian-e2e) with real Trino, the way a user would. It installs the last published chart, upgrades it to your checkout, and then checks:
- pools filling with profile-sized pods
- MCP queries on operator clusters
create_cluster/delete_cluster- reservations over REST and MCP
- scaling
- coordinator crash recovery
- credential rotation
- an MCP rolling restart with no failed calls
make e2e # needs kind, kubectl, helm and docker; ~20 minutes
E2E_POOL_SIZE=1 make e2e # if Docker has less than 12 GB of memory
E2E_KEEP=1 make e2e # keep the kind cluster afterwards for debugging
Every pull request runs the same suite in CI. On failure, the operator, MCP and Trino logs are saved to e2e-artifacts/.
Manual Test Cases
Run these in Claude Desktop with the local environment running.
Cache results (5, 10) only hold within one MCP server process. Restarting Claude Desktop clears them.
Operator Test Cases
Run the operator locally against the kind cluster:
make install && make build
./bin/meridian-operator --namespace meridian --kubeconfig ~/.kube/config
Beyond AI Agents
The MCP server is a standard RPC interface. Any system that automates Trino cluster lifecycle can use it. For example, a CI job can reserve a fresh cluster for integration tests and release it afterwards (pseudo-code, using any MCP client):
- name: Reserve test cluster
run: |
RESULT=$(mcp-client call reserve_cluster \
--workload ci --client-id github-actions --reservation-id ${{ github.run_id }})
echo "CLUSTER_NAME=$(echo $RESULT | jq -r .clusterName)" >> $GITHUB_ENV
echo "COORDINATOR_URL=$(echo $RESULT | jq -r .coordinatorUrl)" >> $GITHUB_ENV
- name: Run integration tests
run: ./test.sh $COORDINATOR_URL
- name: Release cluster
if: always()
run: mcp-client call release_cluster --cluster-name $CLUSTER_NAME --client-id github-actions
The same pattern works as tasks in Airflow, Dagster, or Prefect.
Why Meridian
- There is no official open-source Trino operator. trinodb/trino#396 has been open since 2019.
- There is no official Trino MCP server. trinodb/trino#26239 is open. Meridian covers cluster management, not just query execution.
- One interface for everyone. CI/CD, orchestrators, internal tools, and AI agents all use the same binary.
Roadmap
License
Apache 2.0. See LICENSE.