@millicast/imply-druid-mcp
v1.0.0
Published
Read-only Model Context Protocol (MCP) server for Apache Druid / Imply over stdio. Runnable via npx or Docker.
Readme
imply-druid-mcp
A read-only Model Context Protocol (MCP) server for Apache Druid / Imply.
It speaks MCP over stdio, so it is launched on demand by an MCP client (such as Devin) and exits when the session ends. There is no long-running HTTP MCP server to host or keep alive — you just point the client at a command (either npx or docker run).
Features
- Read-only by design. Only
SELECT,WITH, andEXPLAINstatements are allowed. Writes (INSERT/REPLACE), deletes, and schema changes are rejected before they ever reach Druid, and string literals are ignored during keyword scanning to avoid false positives on data values. - Single statement per call. The Druid SQL endpoint runs one statement; multiple statements are rejected rather than silently truncated.
- Runs via
npxor Docker. No global install needed. - Stdio transport. No ports to expose, no server to babysit.
- Schema discovery tools for schemas, datasources, tables, and columns via
INFORMATION_SCHEMA. - Flexible auth: basic auth (username/password) or bearer token (Imply Polaris API key / auth proxy).
Tools
| Tool | Description | Underlying query |
| --- | --- | --- |
| druid_sql_query | Run any read-only Druid SQL query | SELECT / WITH / EXPLAIN ... |
| explain_query | Return the plan for a query without running it | EXPLAIN PLAN FOR ... |
| list_schemas | List SQL schemas | INFORMATION_SCHEMA.SCHEMATA |
| list_datasources | List queryable datasources (the druid schema) | INFORMATION_SCHEMA.TABLES |
| list_tables | List tables across schemas (optionally one schema) | INFORMATION_SCHEMA.TABLES |
| describe_table | List a table's columns and types | INFORMATION_SCHEMA.COLUMNS |
| status | Check connectivity and report the Druid version | GET /status |
Configuration
All configuration is via environment variables:
| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| DRUID_URL | no | http://localhost:8888 | Base URL of the Druid Router/Broker that serves the SQL API |
| DRUID_SQL_PATH | no | /druid/v2/sql/ | Path to the SQL endpoint (override if behind a proxy/prefix) |
| DRUID_USERNAME | no | — | Basic auth username |
| DRUID_PASSWORD | no | — | Basic auth password |
| DRUID_TOKEN | no | — | Bearer token; overrides basic auth |
| DRUID_TIMEOUT_MS | no | 30000 | Per-request timeout in milliseconds |
| DRUID_CONTEXT | no | — | Inline datasource reference appended to the MCP instructions (see below) |
| DRUID_CONTEXT_FILE | no | — | Path to a file appended to the instructions (handy for Docker mounts) |
See .env.example.
Which port? The SQL API is served by the Druid Router (default
8888) and Broker (default8082). TLS deployments and Imply often use8280/9088or a gateway hostname — setDRUID_URLaccordingly.
Giving the AI persistent datasource knowledge (DRUID_CONTEXT)
The server sends an instructions block to the client on every connect. Anything you put in DRUID_CONTEXT (or a file referenced by DRUID_CONTEXT_FILE) is appended to it, so the AI knows your datasources, dimensions, metrics, units, and semantics without rediscovering them each session.
Set it in the MCP connector's env block, e.g.:
"env": {
"DRUID_URL": "https://druid.example.com:8888",
"DRUID_CONTEXT": "Datasources:\n- requests: per-request events. dimensions: service, endpoint, region. metrics: count, latency_ms (p95 via APPROX_QUANTILE_DS). __time is event time. High cardinality \u2014 always filter __time and service.\n- billing_daily: daily rollup. dimensions: account_id, plan. metrics: revenue_usd."
}For large references, mount a file into the container and point at it:
docker run --rm -i \
-e DRUID_URL -e DRUID_CONTEXT_FILE=/config/druid-context.md \
-v /path/to/druid-context.md:/config/druid-context.md:ro \
your-registry/imply-druid-mcp:latestUsage with Devin (and other MCP clients)
The server is configured as an MCP server using a command + args + env block. Pick one of the two methods below.
Option A — npx (no Docker)
{
"mcpServers": {
"druid": {
"command": "npx",
"args": ["-y", "@millicast/imply-druid-mcp"],
"env": {
"DRUID_URL": "https://druid.example.com:8888",
"DRUID_USERNAME": "readonly_user",
"DRUID_PASSWORD": "••••••"
}
}
}
}
npxrequires the package to be published to a registry (see Publishing). For a private/local checkout, you can instead point at the built file:"command": "node", "args": ["/abs/path/dist/index.js"].
Option B — Docker image (recommended for hosting)
Build and host the image once, then have the client run it on demand:
{
"mcpServers": {
"druid": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "DRUID_URL",
"-e", "DRUID_USERNAME",
"-e", "DRUID_PASSWORD",
"your-registry/imply-druid-mcp:latest"
],
"env": {
"DRUID_URL": "https://druid.example.com:8888",
"DRUID_USERNAME": "readonly_user",
"DRUID_PASSWORD": "••••••"
}
}
}
}The -i flag is required — MCP communicates over stdin/stdout. --rm cleans up the container after each session. Each variable is passed through with -e NAME (value taken from the client's env block).
Building and running the Docker image
This project uses pnpm (pinned via packageManager in package.json and installed through corepack inside the build).
# Build
docker build -t imply-druid-mcp .
# Connectivity self-test (no MCP session; verifies config + reaches Druid)
docker run --rm -e DRUID_URL=https://druid.example.com:8888 \
imply-druid-mcp --self-test
# Run as an MCP server over stdio (normally launched by the client, not by hand)
docker run --rm -i -e DRUID_URL=https://druid.example.com:8888 imply-druid-mcpPush to a registry to host it
docker tag imply-druid-mcp your-registry/imply-druid-mcp:1.0.0
docker push your-registry/imply-druid-mcp:1.0.0Once pushed, any host with Docker can run it on demand via the Option B config above — nothing needs to stay running between sessions.
Local development & testing
Install + build:
corepack enable # makes pnpm available pnpm install pnpm run build # compile TypeScript to dist/Add your connection details to the gitignored
.envfile (URL, username/password, etc.). See.env.examplefor the full list of variables.Verify connectivity (hits Druid
/statususing.env):pnpm run selftestFull end-to-end test — launches the server, runs an MCP handshake, then calls
statusandlist_datasourcesagainst your Druid cluster:pnpm run test:localRun the server directly (stdio, config from
.env):pnpm run start:local
These scripts load
.envvia Node's built-in--env-fileflag (Node 20.6+), so no extra dependency is needed.pnpm run devrunstscin watch mode.
Test the Docker image locally
Once Docker is running, the same .env file works with --env-file:
docker build -t imply-druid-mcp .
docker run --rm --env-file .env imply-druid-mcp --self-test # connectivity check
docker run --rm -i --env-file .env imply-druid-mcp # run as MCP serverQuick manual MCP check (no Druid needed)
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
| node dist/index.jsPublishing to npm
This package is published under the @millicast scope. To enable the npx @millicast/imply-druid-mcp workflow:
npm whoami # ensure you're logged in with access to the @millicast org
pnpm run build
npm publish # publishConfig.access=public + prepublishOnly (rebuilds dist/) are already setRead-only guarantees
- Statement allow-list: the statement must start with
SELECT,WITH, orEXPLAIN. - Single statement: stacked statements are rejected; the SQL endpoint runs one statement per request.
- Keyword deny-list:
INSERT,REPLACE,UPSERT,UPDATE,DELETE,MERGE,DROP,CREATE,ALTER,TRUNCATE,GRANT,REVOKE,KILL,CALL, andINTOare rejected anywhere outside of string literals.
Druid SQL has no transport-level read-only mode (queries are sent via HTTP POST), so for maximum safety also point the server at a Druid user that only holds READ permissions on the datasources you want to expose.
License
MIT
