semql-mcp
An MCP server that wraps a semql Catalog and exposes
its compiler / validator / prompt-renderer surfaces as tools any MCP
client can call. Built on FastMCP.
Two modes
By default the server is compile-only. semql is a pure compiler —
no I/O — and this server keeps that contract. Tools return the emitted
SQL and bound parameters; the caller runs the SQL against whatever
backend they own.
Pass an executor at construction to opt into exec mode. A
query_execute tool registers in addition to the compile-only tools;
it runs the SQL against your executor and returns both the SQL/params
envelope and the resulting rows.
Install
pip install semql-mcp
Quick start — compile-only
from semql import Dialect, Catalog, Cube, Dimension, Measure
from semql_mcp import MCPServer
catalog = Catalog([
Cube(
name="orders",
dialect=Dialect.POSTGRES,
table="orders",
alias="o",
measures=[Measure(name="revenue", sql="{o}.amount", agg="sum", unit="currency")],
dimensions=[Dimension(name="region", sql="{o}.region", type="string")],
),
])
server = MCPServer(catalog)
server.run(transport="stdio") # speak JSON-RPC over stdin/stdout
Quick start — exec mode
Bring your own database driver and adapt its row shape to a list of dicts:
import psycopg
from psycopg.rows import dict_row
from semql_mcp import MCPServer
def executor(sql: str, params: dict) -> list[dict]:
with psycopg.connect("postgresql://...", row_factory=dict_row) as conn:
with conn.cursor() as cur:
cur.execute(sql, params)
return list(cur.fetchall())
server = MCPServer(catalog, executor=executor)
server.run(transport="stdio")
The MCP server never imports a database driver. Whatever you wire in
is what gets called; semql-mcp just hands it (sql, params) and
expects list[dict] back.
Tools
Always registered:
| Tool | Description |
|---|---|
query_semantic(spec, context?) |
Compile a SemanticQuery; return {dialect, sql, params, columns}. |
validate(spec) |
Collect-all static validation; returns list[ValidationError]. Empty when the query would compile cleanly. |
explain(spec, context?) |
Compile and return just the SQL string. |
catalog_prompt(include_introspection=False) |
Render the planner prompt fragment, scoped to the resolved viewer's authorized surface. |
Registered when executor is supplied:
| Tool | Description |
|---|---|
query_execute(spec, context?) |
Compile + run. Returns the query_semantic shape plus rows: list[dict]. Errors carry the SQL we tried to run so callers can replay / inspect it. |
Auto-generated per-cube tools
For each expose_in_prompt=True (non-META) cube, the server also
registers a query_<cube_name> tool whose measures, dimensions,
order (and time_window.dimension, when applicable) parameters are
Literal-typed enums of the cube's actual fields. The planner
sees a JSON Schema with explicit allowed values rather than the bare
list[str] query_semantic accepts.
Field names are bare (no cube prefix); the tool auto-qualifies as
it builds the SemanticQuery:
// query_orders
{
"measures": ["revenue"],
"dimensions": ["region"],
"filters": [{"dimension": "status", "op": "eq", "values": ["paid"]}],
"time_window": {
"dimension": "created_at",
"granularity": "day",
"range": ["2026-01-01", "2026-02-01"]
},
"limit": 100
}
Multi-cube queries (joins across cubes) still go through
query_semantic — the per-cube tools are scoped to a single cube by
construction. When executor is configured, the per-cube tools
return rows too.
Authorization and metadata disclosure
Per-call execution is authorized and fails closed. Pass a viewer_provider
and every tool — query_*, validate, catalog_prompt, lookup, and entity
tools — resolves the viewer and refuses cubes, fields, lookups, and saved
queries it isn't allowed to see. An unauthorized call returns a structured
error, never data or SQL.
Accepted limitation — tool listing is not viewer-filtered. Tools are registered once when the server is constructed, before any client connects, so there is no request identity at registration time. A client that lists tools therefore sees the names, descriptions, and field enums of every role-gated cube, saved query, and entity in the catalog — including ones it cannot execute. This exposes catalog structure (names and shapes), never row data or SQL, and execution remains authorized. Treat tool names and descriptions as non-secret: don't encode confidential facts in cube / field / saved-query names if untrusted clients can list tools. To gate listing itself, run a per-tenant server (one catalog per trust boundary) or front it with a transport that filters the advertised tool set per connection.
In-process testing
FastMCP's Client connects to a FastMCP instance without a transport
— useful for end-to-end testing of your catalog + planner together:
import asyncio
from fastmcp import Client
from semql_mcp import MCPServer
server = MCPServer(catalog)
async def smoke() -> None:
async with Client(server.mcp) as c:
tools = await c.list_tools()
print([t.name for t in tools])
result = await c.call_tool("explain", {"spec": {"measures": ["orders.revenue"]}})
print(result.data)
asyncio.run(smoke())
Status
Early development. The tool surface is stable.
Release files for semql-mcp 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| semql_mcp-0.7.0.tar.gz | 23.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| semql_mcp-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 49.0 kB
Release files / semql_mcp-0.7.0.tar.gz
| Download URL | semql_mcp-0.7.0.tar.gz |
|---|---|
| Size | 23.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d04f9326338c8a5c44a06dfa5223b0c3774efb51d973a2ee759cf7fbad5b896c
|
|
BLAKE2b-256 checksum How to use checksums |
a0c3d79e9b3cb808371cbfbcfa551ddac5c9084b8ed279a25ba95eee25b514bb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 21, 2026.
Transparency logRelease files / semql_mcp-0.7.0-py3-none-any.whl
| Download URL | semql_mcp-0.7.0-py3-none-any.whl |
|---|---|
| Size | 25.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
769615ad67c62269be1d434a15c803ca92b5c3291485d02d8857fa2908ccdb33
|
|
BLAKE2b-256 checksum How to use checksums |
d6c5b2343c927d347e3921a6492f04befa9d95ccaecb785ac116fe5444931c83
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 21, 2026.
Transparency log