Database MCP Server
A Java-based Model Context Protocol (MCP) server for secure database query and management, built on Spring Boot 4.1 and Spring AI 2.0. English · 简体中文
Features
- Multi-Database Support: Oracle, MySQL, PostgreSQL, SQL Server, SQLite, DB2, H2, and Generic JDBC
- BYOK (Bring Your Own Key): Secure connection management with lease-based pooling
- Cross-Database Federation: Query across multiple databases in a single operation
- SQL Audit: Complete execution logging with compliance reporting
- Data Quality: Built-in quality checks with custom rule support
- Change Data Capture: Real-time data synchronization with CDC support
- Lineage Tracking: Data lineage analysis and impact assessment
- Query Optimization: AI-powered SQL optimization recommendations
- Streaming: SSE (Server-Sent Events) streaming for large result sets
- Security: SQL injection prevention, data masking, and RBAC
Quick Start
Docker
docker build -t database-mcp-server:latest .
docker run -d \
--name database-mcp \
-p 8686:8686 \
-e MCP_SECURITY_ADMIN_PASSWORD='replace-with-your-own-password' \
database-mcp-server:latest
The image runs the production profile with authentication on, so the container restart-loops
if no password is supplied. See .env.example for the docker compose variables.
No database environment variables are needed to start. Target databases are registered
at runtime through the createNamedConnection tool (BYOK), so credentials never live in
a config file. Register a connection, confirm it with describeConnection, then query.
Environment Variables
# Server port
SERVER_PORT=8686
# Spring profile — application-production.yml / application-test.yml
SPRING_PROFILES_ACTIVE=production
# JVM options
JAVA_OPTS=-Xms256m -Xmx512m -XX:+UseG1GC
# Required: HTTP Basic password for /mcp (username defaults to admin).
# entropy.mcp.security.enabled defaults to true since 0.4.0, so without a non-blank
# value here the context fails to start. Deliberate: better not to boot than to serve
# /mcp to anyone who can reach the port. An empty value counts as unset.
# Only a real environment variable or -Dmcp.security.admin-password is read — this
# cannot be put in application*.yml, so it never lands in version control.
MCP_SECURITY_ADMIN_PASSWORD=your_admin_password
# Optional: persist the audit log to a database table.
# Without this, audit records are written to file only.
MCP_AUDIT_DB_PASSWORD=your_audit_db_password
Tool exposure and security are configured through entropy.mcp.* properties in
application.yml, not environment variables — see
entropy.mcp.tools.plane, entropy.mcp.security.* and
entropy.mcp.database.ddl.allowed. The one exception is the admin password above:
it is read from the environment only, on purpose.
To run without authentication (local development only — /mcp can execute queries,
DDL and ETL writes against every registered BYOK connection):
java -jar app.jar --entropy.mcp.security.enabled=false
Debugging
The transport is Streamable HTTP, so the official MCP Inspector connects directly — no custom UI needed in this project:
# When the port is bound to the server's loopback only, tunnel first
ssh -L 8686:localhost:8686 <server>
npx @modelcontextprotocol/inspector
# In the UI pick "Streamable HTTP" and enter http://localhost:8686/mcp
# With authentication on, put the admin Basic credentials in the Authorization header
Inspector is two processes: the React UI (port 6274) and the Node proxy MCPP (port 6277);
the browser talks to MCPP, which forwards to /mcp. Do not run it on the server under
test — MCPP is allowed to spawn local processes (that is how stdio transport works), so
setting HOST=0.0.0.0 to reach it remotely opens a remote-execution path on that host.
That is why it binds to localhost and mints a one-time session token by default.
The minimal UI-free check is a single curl (send both Accept types — in STATELESS mode
the response may be an SSE frame):
curl -s -X POST http://localhost:8686/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Three common causes of tool-call errors
MCP Tools
Roughly 130 tools, all registered under the default configuration. The exact count is
deliberately not pinned here — it goes stale the moment a tool is added or removed. Read it off
the startup log (MCP tool exposure: X of Y tools registered) or from the totalTools field
returned by suggestTools. Use
entropy.mcp.tools.plane (all / data / control) plus groups / include /
exclude to narrow the exposed set — see application.yml. Call suggestTools with a
natural-language intent when unsure which tool to use.
Every tool names its connection input connection. This is pinned by
ToolDescriptionContractTest: the Java parameter name is the JSON schema property name, so
renaming a parameter is a contract change. Note that output fields still use
connectionName (e.g. getPoolStats → pools[].connectionName) — that is the response
contract and unrelated to the input name.
Sending every tool description to the model consumes a large amount of context, so
three ready-made presets expose only the groups a given role needs (composable with the
production profile):
--spring.profiles.active=production,tools-explore # query and exploration 23 tools
--spring.profiles.active=production,tools-dba # DBA diagnostics 31 tools
--spring.profiles.active=production,tools-govern # catalog/lineage/quality 36 tools
The startup log prints the retained count and a per-group breakdown. plane: data is an
alternative cut (read-only tools only, about three quarters of the catalogue): the data plane can
list and verify connections, but
createNamedConnection carries the write tag and stays on the control plane, so a pure
data-plane replica needs include: [createNamedConnection] to punch through — or lets a
control-plane replica do the registering.
Note: the 10 tools under "ETL Jobs" are gated by entropy.mcp.gateway.enabled
(default true) and are not registered when it is off. The five connection-management
tools, including createNamedConnection and createSealedConnection, are registered unconditionally.
Query & Export
executeQuery- Paginated SELECT with continuationTokenexecuteQueryWithFilter- Query with filter conditionsexecuteSqlTemplate- Run a predefined SQL templatebatchQuery- Execute multiple queries in one callgetDatabaseInfo- Database version and configurationexportCsv/exportJson- Export result setsexplainPlan- Fetch the execution planassessQueryRisk- Score a query's risk level
Schema Discovery
describe- Generic object description entry pointlistSchemas/listTables/listViews/listSequences- Enumerate objectssearchTables- Search tables by keyworddescribeTable- Column definitions, types, commentslistIndexes- Index details for a tablesuggestTools- Recommend tools from a natural-language intent
⚠️ listTables has no result cap: on a real Oracle schema it returned roughly 1000 tables in
one call (including many _BAK and date-suffixed history tables) — enough to blow up a client's
context. On table-heavy schemas use searchTables with a keyword instead.
DDL & Writes
validateDdl- Validate DDL without executingexecuteDdl/executeDdlBatch/executeDdlRemote- Execute DDLdiffSchema- Diff two schemasbackupData- Snapshot table datainsertData/insertQueryResult/upsertData- Write rowstransformAndInsert- Transform then insertexportQueryToTable- Materialise a query into a table
ETL Jobs
submitEtlJob- Submit an ETL batch jobgetJobStatus/listJobs/stopJob- Job lifecyclevalidateDataQuality- Quality gate inside an ETL pipeline
Connection Management (BYOK)
createNamedConnection- Register a named data sourcecreateSealedConnection- Register from a sealed credential (no password in the conversation)listConnections/describeConnection/getConnectionCount- Inspect connections
Cross-Database & Federation
listDatabases/listRemoteTables/describeRemoteTable- Explore remote sourcesexecuteFederatedQuery- Federated query across databasesexecuteSelectiveQuery- Route a query to a selected sourcequeryCrossDatabaseJoin- Join tables across databasesqueryComplexCrossDatabaseAnalytics- Complex cross-database analyticsgetCrossDatabaseTemplates/getFederatedDatabaseInfo/getGatewayStatistics- Gateway metadatacreateDbLink/testDbLink/dropDbLink- DB link lifecycle
Optimization
analyzeQuery/analyzeTable- Query and table analysisrecommendIndexes- Index recommendationssuggestRewrites- SQL rewrite suggestionsinterpretPlan- Explain an execution plangetOptimizerConfig- Current optimizer settings
Lineage
analyzeLineage- Trace table lineagegetUpstream/getDownstream- Upstream and downstream dependenciesgetImpactAnalysis- Impact of a schema changelistAllEdges- All lineage edgesexportMermaid/exportDot- Export lineage graphsgetLineageConfig- Current lineage settings
Data Quality
checkTableQuality- Run quality checks on a tablelistQualityRuleTemplates- Available rule templatesgetQualityAlertSummary- Aggregated quality alerts
Data Catalog
generateCatalog/scanSchema- Build and refresh the catalogsearchAssets- Search data assetsclassifyColumn/listSensitiveColumns- Sensitivity classificationgetCatalogConfig- Current catalog settings
Change Data Capture
checkCdcSupport- Check whether the database supports CDCregisterSubscription/listSubscriptions/unregisterSubscription- SubscriptionsreadChanges/getCurrentLsn- Read change events and log positioncreateMirrorTable- Create a mirror tablegetCdcStatus/getCdcConfig- CDC status and settings
Backup & Restore
backupTable/backupSchema- Back up table data (as replayable INSERTs) or a table's DDLrestoreBackup/quickRestore- Replay a backup (append) or restore a whole table (delete first)listBackups/getBackup- Inspect backupsdeleteBackup/cleanupBackups- Remove backupsgetBackupConfig- Current backup settings and in-memory storage limits
⚠️ Backup records live in process memory only — nothing is written to disk or to a
database table, and everything is lost on restart. They are bounded by
entropy.mcp.database.backup.max-records (200) and retention-days (7); records beyond
either bound are evicted silently, after which restoreBackup / quickRestore return
NOT_FOUND. For durable backups, save the sqlStatements returned by backupTable
yourself.
DBA Diagnostics
checkHealth- Database health checklistActiveSessions/killSession- Session inspection and terminationshowLocks/showBlockingTree- Lock and blocking analysisshowUndoUsage- Undo consumptionlistTablespaces/listDataFiles- Storage layoutestimateTableSize/gatherTableStats- Sizing and statisticsshowIndexStatus/listInvalidObjects- Object stateflashbackQuery- Flashback querylistCurrentPrivileges/listGrants- Privilege inspection
Audit & Monitoring
getAuditLogs/getAuditMetrics- Audit trail and metricsgetSlowQueries/getSqlPatternStats- Slow query and pattern statisticsgetDataAccessReport/getProtectionReport- Compliance reportinggetPoolStats/getPoolStatsForConnection- Connection pool statisticsgetMetrics/getStatistics- Runtime metricsclearCache- Clear caches
Session & Custom Tools
sessionStore/sessionGet/sessionKeys/sessionRemove/sessionPurge- Scoped key-value storegetSessionInfo- Inspect the current scopelistCustomTools/scanCustomTools/invokeCustomTool- User-defined SQL tools
Architecture
Maven multi-module layout with a one-way dependency graph enforced at build time by ArchUnit:
database-mcp-contract/ # Zero-dependency leaf: dialect contracts
database-mcp-core/ # Configuration properties, session context, exception hierarchy
database-mcp-dialect/ # Dialect adapters (Oracle / MySQL / PostgreSQL / SQL Server)
database-mcp-infra/ # Data sources, pooling, caching, audit persistence
database-mcp-features/ # ETL / CDC / lineage / quality / backup / optimizer / catalog
database-mcp-tools/ # MCP tool implementations and exposure filtering
database-mcp-app/ # Spring Boot entry point and configuration
Dependency order: contract <- core <- infra <- features <- tools <- app
Security
HTTP Authentication
entropy.mcp.security.enabled defaults to true (breaking change introduced in 0.4.0, still in
effect). With it on:
/mcprequires an authenticated principal/api/**requiresROLE_ADMIN- every actuator endpoint other than
healthandinforequires authentication - anything else is denied
HTTP Basic is always wired; Bearer/JWT is added as well when any
spring.security.oauth2.resourceserver.jwt.* property is set. health and info stay
anonymous so that liveness and readiness probes keep working.
Credential Handling
Target-database connections come in three flavours, and the difference is a security boundary, not just convenience:
- Declared (preferred) —
entropy.mcp.database.connections.<name>inapplication.yml, with the password supplied through a${ENV_VAR}placeholder. Pinned pools are built at startup and never expire. Only the connection name ever appears in a conversation; no credential passes through the model. - BYOK —
createNamedConnectionat runtime. The password is a tool argument, so it enters the model's context and stays in whatever conversation history the client keeps. Use this for ad-hoc access only. - BYOK + sealed credentials —
createSealedConnectionat runtime, passing only a connection name, a jdbcUrl and one sealed credential blob. Username and password are encrypted inside the blob and opened server-side with a private key: nobody calling the tool needs to know the password, and the model and conversation history only ever see base64 that cannot be reversed without that key. See Sealed Credentials.
Never embed credentials in the JDBC URL (?password=, ;PWD=, user/pw@): the URL is echoed back
by listConnections, describeConnection, getPoolStats and getDatabaseInfo.
Those four do run every URL through JdbcUrlMasker first — userinfo becomes **** and parameters
are filtered by allowlist, keeping only keys that cannot carry a secret (databaseName,
currentSchema, sslMode, …) — but relying on the masker is strictly worse than never sending the
secret at all.
All three flavours share everything downstream: the same ByokDataSourceFactory, hence the same SQL
validation, masking, auditing and statement timeouts.
Sealed Credentials
The server holds the private key; the operator holds the public key. The direction is
deliberately the opposite of Druid's ConfigTools: Druid puts the decryption material (the public
key) next to the ciphertext in the config, so the ciphertext only defeats a casual reader. Here the
ciphertext travels through the model and the conversation history, so holding it must not be enough
to recover the password.
# 1. Generate a key pair. The private key is written to the file given by --out (mode 600; the file
# is a single `MCP_CREDENTIAL_PRIVATE_KEY=...` line), and stdout carries the public key only.
# The private key deliberately never goes to stdout: stdout survives in CI logs and terminal
# scrollback, and that key is the entire security of this mechanism. Without --out it defaults to
# ./mcp-credential-private.key. Delete the file once it has been injected into the server.
java -cp database-mcp-server-0.6.0.jar \
-Dloader.main=com.entropy.database.mcp.cli.CredentialSealCli \
org.springframework.boot.loader.launch.PropertiesLauncher \
keygen --out mcp-credential-private.key
# 2. Seal one credential. The password is read from stdin, never from a CLI flag
# (flags are visible in `ps` and land in shell history).
java -cp database-mcp-server-0.6.0.jar \
-Dloader.main=com.entropy.database.mcp.cli.CredentialSealCli \
org.springframework.boot.loader.launch.PropertiesLauncher \
seal --public-key public.key --name orders-prod \
--url 'jdbc:postgresql://db-host:5432/orders' --username app_ro --ttl 15m
Server configuration — application.yml already wires the placeholder, so a real deployment only
needs the MCP_CREDENTIAL_PRIVATE_KEY environment variable. Unset means an empty value, which means
the feature is off and the server starts normally:
entropy:
mcp:
security:
credential-cipher:
private-key: ${MCP_CREDENTIAL_PRIVATE_KEY:} # environment only, never in a file
max-ttl: 15m # longest remaining lifetime accepted at runtime
require-sealed-credentials: false # true ⇒ createNamedConnection rejects plaintext
A sealed blob is bound to the connection name, the jdbcUrl (compared byte for byte) and an expiry. All three are load-bearing:
- without the name binding, anyone holding the blob could register it under a connection name of their own choosing and read through it;
- without the URL binding, an attacker could point the blob at a database they control and have the server hand the decrypted password over to it;
- without an expiry, that base64 string is a permanent password sitting in conversation history.
Known tradeoff: within its lifetime the same blob can be replayed — there is no single-use
consumption, which would require the server to remember spent blobs (lost on every restart) and would
break the normal flow where a caller re-registers with the same credential after a BYOK lease expires.
The default 15 minutes is that replay window. With no private key configured the whole mechanism is
off and createSealedConnection fails immediately, pointing back at createNamedConnection.
Sealed blobs also work for pinned connections, which keeps the password out of the environment too:
entropy:
mcp:
database:
connections:
orders-prod:
jdbc-url: jdbc:postgresql://db-host:5432/orders
username: app_ro
password: sealed:BASE64... # opened at startup; username must match the sealed one
Two boundaries worth stating: readonly: true is enforced by this server (write tools
return CON005); it says nothing about the database account's actual privileges, so do not
treat it as a substitute for least privilege. And listConnections / describeConnection
still echo leaseExpiry and maxLifetimeExpiry for pinned connections even though neither
applies (LeasedDataSource.pinned is exempt from both) — a display leftover; do not
re-register on account of it.
SQL Validation
All queries are validated using JSqlParser to prevent SQL injection. The system enforces:
- Operation whitelisting (DQL/DDL/DML)
- Table name validation
- Parameterized query enforcement
- Max results limiting
Data Masking
Sensitive data can be automatically masked based on configurable rules.
Audit Logging
Complete execution audit trail with:
- Query timestamp
- Executing user
- Source IP
- Execution time
- Result count
- Row-level changes
Development
Build
mvn clean package -DskipTests
Run Tests
mvn test
Docker Build
docker build -t database-mcp .
docker compose up -d
Configuration
See application.yml for full configuration options.
Multi-Environment Profiles
- default -
application.yml test-application-test.ymlproduction-application-production.yml
Tool Exposure
entropy:
mcp:
tools:
plane: all # all / data / control; any other value fails startup
groups: []
include: []
exclude: []
plane is derived from the tags in each tool description: data keeps read-only tools
(read present, none of write / ddl / destructive / admin), control is the
complement. Unknown names in include / exclude / groups fail startup rather than
being silently ignored.
Audit Persistence (optional)
Auditing lands in three places: the audit log file, an in-memory ring buffer (last 100 entries, for
SSE/polling), and the audit_log table. Only the table is queryable as history by getAuditLogs /
getDataAccessReport / getProtectionReport.
The first two are always on. The table is opt-in: with no spring.datasource.url there is no
audit_log at all and auditing stays file-plus-buffer. Two ways to turn it on:
Option 1 — external database (durable; use this for a compliance-grade trail)
spring:
datasource:
url: jdbc:postgresql://audit-db:5432/mcp_audit
username: mcp
password: ${MCP_AUDIT_DB_PASSWORD}
Option 2 — in-process H2 (no external dependency; just enough to exercise the audit query tools locally, in demos, or in smoke tests)
spring:
datasource:
url: jdbc:h2:mem:mcp_audit;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password: ""
With option 2 the history dies with the process and is not shared across replicas. Startup logs
a Audit log is persisted to an IN-MEMORY database (...) warning saying exactly that.
Either can be set without touching the file, via --spring.datasource.url=... or the
SPRING_DATASOURCE_URL / SPRING_DATASOURCE_USERNAME / SPRING_DATASOURCE_PASSWORD environment
variables.
Notes:
- No manual DDL needed.
AuditLogRepositorydetects the product family (a separateCREATE TABLEfor H2 / PostgreSQL / MySQL / Oracle / SQL Server) and createsaudit_logon demand. The account needs DDL privileges; if the table cannot be created, auditing degrades to file-only. - Do not point the audit database at a database being audited. It muddles ownership, and such accounts usually lack DDL privileges anyway.
DB_CLOSE_DELAY=-1is required, not a tuning knob: without it H2 drops the whole database when Hikari reclaims the last idle connection, which shows up as audit rows silently going missing.- Upgrading a deployment that already has an
audit_logtable: columns were renamed (sql→sql_text,rows→row_count,timestamp→event_time). See theALTER TABLEexamples in theAuditLogRepositoryclass javadoc.
Target database connections are not configured here — they are registered at runtime via
createNamedConnection.
Read-Only Web UI
The server serves a small read-only operations page at the root path: open
http://<host>:8686/ in a browser. Four tabs:
GET /api/ui/config returns {authEnabled, auditPersistence, maxLimit}; the page drives both the
banner and the "audit history unavailable" state from it rather than hardcoding either.
⚠️ This page widens the unauthenticated surface. With entropy.mcp.security.enabled=false the
page and every endpoint it calls are open to anyone who can reach the port — including the raw SQL in
the audit trail, the raw SQL of slow queries, and the full list of registered connections. These are
not new permissions (/api/audit/** was always at this level), but it turns "readable if you know
the /api/audit/logs path" into "visible on opening the root URL", and in practice that difference
is the exposure. So:
- Running unauthenticated, the page shows a non-dismissible red banner saying exactly that. Its
content is derived from
authEnabledin/api/ui/config, not hardcoded. The startupMCP HTTP authentication is DISABLEDbanner now names the UI too. - The only switch that closes this is
entropy.mcp.security.enabled=true(which also requiresMCP_SECURITY_ADMIN_PASSWORD). With it on,SecurityConfigputs/api/**and the page's static resources (/,/index.html,/assets/**) in the sameROLE_ADMINbucket, and the browser prompts for Basic credentials. The third entry is a wildcard rather than a file list because Vite emits content-hashed assets (assets/index-<hash>.js): an exact list silently 403s the whole page on every authenticated deployment after each frontend rebuild. - They are always the same bucket: there is no "page loads but every table is 401" middle state.
Conversely, with authentication on and no credentials,
/is a 401 and the frontend renders that 401 verbatim instead of an empty table that reads like "no data".
Deliberate implementation choices — read these before changing this area:
- The frontend is built. Vite + React 19 + the Meta Astryx
design system. Sources live in
database-mcp-app/src/main/frontend/;frontend-maven-pluginrunsnpm ci && npm run buildduringgenerate-resourcesand the output lands intarget/classes/static/, shipped inside the fat jar. The plugin installs its own node/npm (never the machine's) undersrc/main/frontend/node/rather thantarget/, so acleandoes not force another download.- Every dependency is pinned to an exact version (no
^, no~) andpackage-lock.jsonis committed: Astryx is Beta. mvn -o(offline) inherently conflicts with a frontend build, hence the-Dfrontend.skip=trueescape hatch. It deliberately does not also skip tests: when the output is missing,WebUiStaticAssetsPresenceTestfails and explains why, so a jar with an emptystatic/cannot ship silently. For an offline Java-only build, omitclean:mise exec -- mvn -o install -Dfrontend.skip=true.- Two Astryx 0.6.2 traps are recorded in code comments: the published docs' import
@astryxdesign/theme-neutral/cssdoes not exist in 0.6.2 (the real entry is/theme.css, seesrc/main.jsx), and the theme CSS is wrapped in@scope ([data-astryx-theme="neutral"]), soindex.htmlmust carry that attribute on<html>or the page renders completely unstyled without raising an error.
- Every dependency is pinned to an exact version (no
- No CDN links. The target runs in an air-gapped-ish Kubernetes namespace where anything fetched from the internet simply fails. Use inline SVG if a chart is ever needed.
- Auto-refresh is off by default. Every refresh of the connections tab goes through read methods
on the connection registry, and
PerformanceTimingAspectfeeds those intorecordToolExecution— a fixed poll would have the page polluting the very metrics on its own performance tab. - JDBC URLs are always the masked copy. Every URL on the page comes from
ConnectionMetadata.jdbcUrlMasked/HikariPoolStats.jdbcUrlMasked, both produced throughJdbcUrlMaskerwhen the pool is built.DatabaseHealthMonitor.getHealthStatus()is deliberately not exposed: it puts an unmaskedDatabaseMetaData.getURL()into its result, and it describes the default datasource (the audit database) rather than the BYOK connections this page is about. Connection health is expressed throughisPoolHealthy/healthWarnings/degradedPoolsfrom the pool statistics instead. /does not shadow/mcpor/actuator/**. The static resource handler has the lowest precedence; the MCP protocol endpoint is still/mcp.
Verifying that the page is also present in the packaged fat jar (the classic failure mode for this kind of change is "fine in the IDE, 404 in the jar"):
java -jar database-mcp-app/target/database-mcp-server-0.6.0.jar \
--spring.profiles.active=test --server.port=18700 --entropy.mcp.security.enabled=false
curl -si http://localhost:18700/ | head -1 # 200, text/html
curl -s http://localhost:18700/api/ui/config # {"authEnabled":false,"auditPersistence":false,...}
curl -s http://localhost:18700/api/ui/connections
curl -s http://localhost:18700/api/ui/performance?limit=5
curl -si http://localhost:18700/api/audit/history | head -1 # 503, audit not persisted
API Reference
Health Check
GET /actuator/health
Anonymous, so probes keep working with authentication enabled.
MCP Endpoint
The path is /mcp, not /mcp/message: spring.ai.mcp.server.protocol is STATELESS,
which serves a single endpoint. With authentication on, any other path is denied outright,
so a request to /mcp/message returns 403 rather than 404.
POST /mcp
Content-Type: application/json
Accept: application/json, text/event-stream
Authorization: Basic YWRtaW46eW91cl9hZG1pbl9wYXNzd29yZA==
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "executeQuery",
"arguments": {
"connection": "primary",
"sql": "SELECT id, name FROM users ORDER BY id",
"maxRows": 100
}
},
"id": 1
}
License
Copyright 2024-2026 Entropy. Licensed under the Apache License, Version 2.0.
Contributing
- Fork the repository
- Create a feature branch
- Commit your changes
- Push to the branch
- Create a Pull Request
Support
- GitHub Issues: Report bugs and feature requests