Python SDK

React and MCP clients

Serve Python datasets to generated, type-safe React hooks and to AI agents through the MCP bridge.

A Python backend can feed the same @hypequery/react hooks and hypequery mcp server that TypeScript backends use. Python keeps ownership of SQL, authentication and the tenant. The frontend and the agent only ever see a client contract: a JSON description of each endpoint's fields, operators, limits and schema hash.

StepWhereCommand or API
1. Declare client endpointsPythonClientEndpointDefinition in a definition-only module
2. Install the endpointsPythonadd_dataset_endpoint(..., client_profile=...), add_metric_endpoint, add_client_contract_endpoint
3. Export the contractPython, then frontendpython -m hypequery export client-contract, then npx hypequery generate:client
4. Connect MCPAnywhere Node runsnpx hypequery mcp --client-contract-url https://…/v1/client-contract

The Python side needs no Node process. Generating the React client and running the MCP bridge do; there is no native Python MCP server. On the frontend, see React: datasets served from Python for per-login clients, cache scope and result types.

1. Declare client endpoints

Keep datasets and client endpoint declarations in a module that imports nothing from hypequery.serve, a database driver or your credentials. The export command imports it, so it must be cheap and side-effect free.

# analytics/definitions.py
from hypequery.datasets import (
    ClientEndpointDefinition,
    ClientQueryProfile,
    belongs_to,
    create_dataset_registry,
    dataset,
    dimension,
    export_client_contract,
    measure,
)

customers = dataset(
    "customers",
    source="customers",
    tenant_key="org_id",
    dimensions={"country": dimension.string(), "segment": dimension.string()},
    measures={"customerCount": measure.count_distinct("customer_id")},
)

orders = dataset(
    "orders",
    source="orders",
    tenant_key="org_id",
    time_key="created_at",
    dimensions={
        "status": dimension.string(),
        "createdAt": dimension.timestamp(column="created_at"),
    },
    measures={"revenue": measure.sum("amount"), "orderCount": measure.count("order_id")},
    relationships={
        "customer": belongs_to(lambda: customers, from_field="customer_id", to_field="customer_id")
    },
)

registry = create_dataset_registry(orders, customers)
profile = ClientQueryProfile(default_limit=50, max_limit=500)
RELATED = frozenset({"customer"})


def build():
    """Zero-argument factory for `hypequery export client-contract`."""
    return export_client_contract(
        [
            ClientEndpointDefinition(orders, "/v1/orders", profile, related=RELATED),
            ClientEndpointDefinition(orders, "/v1/revenue", profile, measure="revenue", name="revenue"),
        ],
        registry=registry,
    )
  • ClientEndpointDefinition(dataset, path, profile) describes a dataset endpoint. Add measure= (and optionally name=, the metric ID and output alias) for a metric endpoint fixed to one measure.
  • ClientQueryProfile sets the default and maximum page size clients may request. Dataset limits and the endpoint policy can only lower it.
  • Related fields (customer.country, related measures) are left out unless related= names the relationship.
  • Pass a registry when definitions reach other datasets through relationships.

2. Install the endpoints

Install each endpoint with client_profile= at the same path the definition declares, and pass the same relationships as include_related=. Add add_client_contract_endpoint for live discovery, and allow your frontend's origin with CorsPolicy:

# analytics/api.py
from hypequery.datasets import create_dataset_client
from hypequery.execution import ClickHouseConnection, create_clickhouse_executor
from hypequery.serve import (
    CorsPolicy,
    EndpointPolicy,
    HttpSecurity,
    add_client_contract_endpoint,
    add_dataset_endpoint,
    add_metric_endpoint,
    create_api,
    to_asgi_app,
)

from .auth import authenticate  # your credential lookup, or jwt_strategy(...)
from .definitions import RELATED, orders, profile, registry

executor = create_clickhouse_executor(ClickHouseConnection.from_env())
client = create_dataset_client(executor=executor, registry=registry)
policy = EndpointPolicy(tenant="required", required_scopes=frozenset({"analytics:read"}))

api = create_api(
    auth=authenticate,
    security=HttpSecurity(
        allowed_hosts=("analytics.example.com",),
        cors=CorsPolicy(origins=("https://app.example.com",)),
    ),
)
add_dataset_endpoint(
    api, "/v1/orders", dataset=orders, client=client, policy=policy,
    client_profile=profile, include_related=RELATED,
)
add_metric_endpoint(
    api, "/v1/revenue", dataset=orders, client=client, policy=policy,
    measure="revenue", name="revenue", client_profile=profile,
)
add_client_contract_endpoint(api, "/v1/client-contract", policy=policy)

app = to_asgi_app(api)

CorsPolicy already allows the X-Hypequery-Client-Schema header that generated clients send. The same API can be served from Django with urls(api) or Flask with blueprint(api) instead of to_asgi_app. Strict client endpoints are checked when the adapter is created, so a mismatch fails at startup. Close the executor at shutdown, for example with to_asgi_app(api, lifespan=...).

Keep both sides in step

The schema hash covers the path, profile and related fields. If related= and include_related= (or the paths) differ, the exported contract does not match the live endpoint, and every request from the generated client gets 409 CLIENT_SCHEMA_CHANGED.

Endpoints installed without client_profile keep their ordinary request handling and are left out of client contracts.

3. Export the contract

Export from the definition module, without starting a server or opening a connection:

python -m hypequery export client-contract \
  --factory analytics.definitions:build \
  --output web/client-contract.json

Run it from the directory that makes analytics importable. Use --check in CI to fail when definitions changed but the committed contract did not, and --force to replace an existing file. See Python CLI.

Alternatively, export from the configured API with hypequery.serve.export_client_contract(api) (the API, or its to_asgi_app or to_wsgi_app application). It uses the installed paths and effective limits, and produces the same build contract as build() above.

The live /v1/client-contract route returns a different, authorized view for the calling principal. That view is for MCP and discovery; generate:client refuses it, because a build must come from the definition owner.

If the API is mounted under a prefix, pass it as mount_path= to export_client_contract(...) and as client_mount_path= to create_api(...) so exported paths match the URLs browsers call.

Then generate the React client in the frontend project, as described in React: datasets served from Python:

npm install -D @hypequery/cli
npx hypequery generate:client client-contract.json --output src/hypequery-client.ts

4. Connect MCP

The MCP bridge reads the live, authorized contract and turns tool calls into the same HTTP requests a React client sends. Each bridge process acts as one principal:

npm install @hypequery/mcp
npm install -D @hypequery/cli
HYPEQUERY_API_KEY=… npx hypequery mcp --client-contract-url https://analytics.example.com/v1/client-contract

See MCP: datasets served from Python for URL rules, --self-test and client configuration.

What the generated types guarantee

Generated hooks are checked against the contract at compile time:

  • Only declared endpoints, dimensions, measures and orderBy fields are accepted. Related fields appear only on endpoints that opted in, and there is no tenant input at all.
  • Filter values follow each field's type and operator; between takes exactly two values and in at least one.
  • Metric endpoints take no measures; dataset endpoints accept having on their measures.
  • Rows contain exactly the selected fields: omitted measures select the endpoint's defaults, and measures: [] selects none. Fields named in a literal selection are always present; fields picked from a runtime string[] are optional.
  • Measure values are string | null, so decimals keep their precision. Numeric dimensions are number | string | null; timestamps are strings. Base dimensions include null, because the contract does not declare columns non-nullable.

Types describe what the server accepts; a contract describes capabilities and grants no permission. Python still authorizes, validates and clamps every request.

A label and description on datasets, dimensions and measures travel in the contract as presentation annotations: list_datasets shows each dataset's description to agents, get_dataset_schema includes field text, and generate:client emits it as JSDoc. Editing that text changes the view hash but not an endpoint's schema pin, so it never triggers a 409.

Keeping clients in sync

ChangeWhat catches it
Python definitions changed, contract not re-exportedhypequery export client-contract … --check in CI
Contract re-exported, client not regeneratednpx hypequery generate:client … --check in CI
Deployed server ahead of a running frontendThe server answers 409 CLIENT_SCHEMA_CHANGED; rebuild and redeploy the frontend

Each request carries its endpoint's schema hash in X-Hypequery-Client-Schema, so a stale client never receives data in a shape its types don't describe. When debugging a 409, compare per-endpoint schemaHash values: they are identical in the exported contract and the live discovery route. The document-level contentHash (and the generated clientContractHash) always differs between the two, because the build view lists every endpoint and the live view only those the caller may use. Neither the hooks nor the MCP bridge automatically adopts a changed contract: the hooks keep their generated types, and the bridge re-reads the live contract but refuses one that changed (MCP_STALE_CONTRACT) until you restart it.

A complete runnable version of this setup, with two tenants, a React app and MCP checks, is in examples/client_dashboard.

On this page

Get early access to hypequery cloud.

Cloud hosts the same model you run in your backend, with a managed API and MCP server, versions, and rollback. We're letting teams in a few at a time. Join the waitlist and we'll reach out when your project can go live.

How do you want to build your analytics layer? (Optional)
What features do you most need? (Optional)

Pick as many as you like.

Only your email is required. All other questions are optional.