orionbelt-semantic-layer-mcp
by ralforion
README.md
<!-- mcp-name: io.github.ralforion/orionbelt-semantic-layer-mcp -->
<p align="center">
<img src="https://raw.githubusercontent.com/ralforion/orionbelt-semantic-layer-mcp/main/docs/assets/ORIONBELT_Logo.png" alt="OrionBelt® Logo" width="400">
</p>
<h1 align="center">OrionBelt® Semantic Layer MCP</h1>
<p align="center"><strong>Thin MCP server that delegates to the OrionBelt® Semantic Layer REST API</strong></p>
[](https://github.com/ralforion/orionbelt-semantic-layer-mcp/releases)
[](https://github.com/ralforion/orionbelt-semantic-layer)
[](https://www.python.org/downloads/)
[](https://github.com/ralforion/orionbelt-semantic-layer-mcp/blob/main/LICENSE)
[](https://gofastmcp.com)
[](https://docs.pydantic.dev)
[](https://docs.astral.sh/ruff/)
[](https://cloud.google.com/bigquery)
[](https://www.postgresql.org)
[](https://www.snowflake.com)
[](https://clickhouse.com)
[](https://www.dremio.com)
[](https://www.databricks.com)
[](https://duckdb.org)
[](https://www.mysql.com)
[](https://hub.docker.com/r/ralforion/orionbelt-semantic-layer-mcp/tags)
[](https://hub.docker.com/r/ralforion/orionbelt-semantic-layer-mcp)
[](https://hub.docker.com/r/ralforion/orionbelt-semantic-layer-mcp/tags)
A thin MCP server that delegates all business logic to the [OrionBelt® Semantic Layer](https://github.com/ralforion/orionbelt-semantic-layer) REST API via HTTP. No embedded engine — pure API pass-through.
## Architecture
The OrionBelt® Semantic Layer platform has two deployment modes. This MCP server supports both:
- **Standalone** — Deploy the [OrionBelt® Semantic Layer API](https://github.com/ralforion/orionbelt-semantic-layer) anywhere (Cloud Run, Docker, localhost) and point this MCP server at it via `API_BASE_URL`.
- **Hosted** — Connect to the public Cloud Run deployment with zero local setup (see [Hosted MCP Server](#hosted-mcp-server) below).
```
┌────────────┐ ┌──────────────────────────────────────────────────────┐
│ LLM Client │ │ OrionBelt® Platform │
│ │ │ │
│ Claude, │──MCP──│──> server.py ──HTTP /v1──> Semantic Layer REST API │
│ Cursor, │ │ (FastMCP (FastAPI: parse OBML, │
│ any MCP │ │ + httpx) validate, compile │
│ client │ │ to SQL) │
└────────────┘ └──────────────────────────────────────────────────────┘
```
- **No business logic** — all tool calls delegate to the REST API (v1 endpoints)
- **Dual-mode** — auto-detects single-model or multi-model API mode at startup
- **Auto-session management** — creates an API session on first tool call, caches the ID (multi-model mode)
- **17 tools** (single-model mode) or **21 tools** (multi-model mode) for querying (QueryObject), execution, batch, discovery, composability (ACR), examples, diagrams, RDF/SPARQL, OSI export, model validation (offline or against the live datasource), and OBML reference + function catalog + JSON schemas. (22 distinct tools exist in total; the API mode selects which subset is active — they overlap in 16 — and no client ever sees all 22 at once.) The visible surface is narrowed further in the design-time phase and when query execution is disabled (see [Design-time vs run-time tool switching](#design-time-vs-run-time-tool-switching))
- **4 prompts + 2 resources** for OBML / OBSQL reference and usage guidance
<p align="center">
<img src="https://raw.githubusercontent.com/ralforion/orionbelt-semantic-layer-mcp/main/docs/assets/architecture.png" alt="OrionBelt® Analytics Architecture" width="900">
</p>
## Live Demo
A public demo of the OrionBelt® Semantic Layer API is available at:
> **API endpoint:** `https://orionbelt.ralforion.com` — [Swagger UI](https://orionbelt.ralforion.com/docs) | [ReDoc](https://orionbelt.ralforion.com/redoc) | [Gradio UI](https://orionbelt.ralforion.com/ui/?__theme=dark)
Set `API_BASE_URL=https://orionbelt.ralforion.com` in your `.env` file to use it (see `.env.example`).
## Installation
```bash
uv sync
```
For development (includes pytest, respx, ruff):
```bash
uv sync --all-groups
```
## Usage
### stdio (default)
```bash
uv run server.py
```
### HTTP transport
```bash
MCP_TRANSPORT=http uv run python server.py
```
The HTTP transport runs **stateless** by default (`MCP_STATELESS_HTTP=true`):
every request is self-contained, with no `Mcp-Session-Id`, no stream
resumability, and no server-initiated messages. Nothing here needs that state —
the tool phase is derived from explicit loaded-model state rather than from the
connection, and no tool sends progress, sampling, or elicitation requests — so
instances can sit behind a load balancer without session affinity. Set
`MCP_STATELESS_HTTP=false` to restore per-connection sessions; `sse` always runs
stateful, since it is a long-lived per-connection stream.
Note this is the *transport* session only. The upstream API session and the set
of loaded models remain process-global and shared by every client of an
instance, unchanged by this flag.
#### Transport security
The HTTP/SSE transport terminates **no TLS** and authenticates **no caller** —
it expects an ingress in front that does both. These are two separate jobs, and
no platform does both for you by default.
On Cloud Run, the intended deployment, **TLS is automatic**: the service URL is
served over HTTPS and plain HTTP is forwarded to the container. **Access control
is not.** It is a deploy-time choice, settled any of three ways — IAM invoker
permission, an `--ingress` restriction, or an authenticating load balancer in
front. With none of them, `--allow-unauthenticated` leaves the service reachable
by anyone who learns its URL, over HTTPS, with no credential required. Encrypted
is not the same as restricted.
This matters because the two hops have different answers:
| Hop | Security |
| --- | --- |
| MCP client → this server | `stdio`: pipes to a child process, no socket at all. `http`/`sse`: whatever the ingress in front provides — nothing if there is none |
| This server → the API | TLS whenever `API_BASE_URL` is `https://` (the default), with certificates verified against httpx's default CA bundle (certifi), or against `API_CA_CERT` for a private CA; `API_CLIENT_CERT` adds a client certificate for mutual TLS. There is no `verify=False` and no way to disable it short of an `http://` URL |
Exposing the HTTP transport directly is the case to avoid. Anything that can
reach the port can call every registered tool — including `execute_query` where
the capability is enabled — spending this server's own `API_KEY` against the
API, over a channel readable in transit. The credential never crosses that hop;
the access it buys does.
The server warns at startup when it detects this: an `http`/`sse` transport
bound to a non-loopback address, off Cloud Run, without `MCP_BEHIND_PROXY=true`.
Bind `MCP_SERVER_HOST` to loopback, put an authenticating TLS ingress in front,
or set `MCP_BEHIND_PROXY=true` to acknowledge a proxy the server has no way to
see.
On Cloud Run it logs an informational note instead of that warning, naming the
half that is still yours: TLS is handled, access control is whatever you
deployed with. **Silence there is not evidence the service is restricted** — the
process cannot read its own IAM policy or ingress setting. Confirm one of the
three above is in place; if one is, `MCP_BEHIND_PROXY=true` records that and
silences the note.
> The API's own `*_TLS_*` settings are the *server* half and none of them is read
> here. `PGWIRE_TLS_*` and `FLIGHT_TLS_*` (2.28.0) secure listeners this server
> never connects to. `API_TLS_*` (2.29.0) makes the REST API serve HTTPS itself,
> and `API_TLS_CLIENT_CA` makes it *require* a client certificate — which is
> what the settings below are for.
#### TLS to the API
When the API serves HTTPS with a certificate from a private CA, or requires
mutual TLS — `API_TLS_CLIENT_CA` on the API, or a gateway in front that asks
for a client certificate — point this server at the material:
```bash
API_BASE_URL=https://obsl.internal:8000
API_CA_CERT=/certs/ca.crt # trust this CA instead of the default store
API_CLIENT_CERT=/certs/mcp.crt # present this certificate…
API_CLIENT_KEY=/certs/mcp.key # …with this key (omit if it is in the same PEM)
```
The certificate is loaded when the server starts, on every transport, so a path
that is missing, unreadable, or not what it claims stops startup naming the
setting and the file — rather than surfacing later as an SSL error from inside
the HTTP client. The startup banner's `API TLS:` line reports what the hop
ended up with.
- **`API_CA_CERT` replaces the default trust store, it does not extend it.**
Unset, verification uses httpx's default (certifi, or `SSL_CERT_FILE` /
`SSL_CERT_DIR`), and adding a client certificate does not change that.
- **These require an `https://` `API_BASE_URL`.** On `http://` there is no
handshake, so nothing would be presented or verified; that configuration is
refused rather than left to read as TLS.
- **Password-protected keys are refused, not prompted for.** Under `stdio`,
stdin is the MCP pipe, so OpenSSL's terminal prompt would corrupt the protocol
or hang. Provide the key decrypted, protected by file permissions or a
secret mount.
### MCP client configuration
Add to your MCP client config (e.g. `claude_desktop_config.json`):
```json
{
"mcpServers": {
"orionbelt": {
"command": "uv",
"args": ["run", "python", "server.py"],
"cwd": "/path/to/orionbelt-semantic-layer-mcp"
}
}
}
```
## Configuration
Environment variables or `.env` file (pydantic-settings). See `.env.example` for defaults.
| Variable | Default | Description |
| ----------------- | ------------ | ------------------------------------- |
| `API_BASE_URL` | — (required) | OrionBelt® Semantic Layer REST API URL |
| `API_KEY` | — (unset) | API credential; required only when the API runs with `AUTH_MODE=api_key` |
| `API_KEY_HEADER` | `X-API-Key` | Header the credential is sent in; must match the API's `API_KEY_HEADER` |
| `API_CA_CERT` | — (unset) | PEM CA bundle to verify the API against, **replacing** the default trust store; for a private CA. Requires an `https://` `API_BASE_URL` — see [TLS to the API](#tls-to-the-api) |
| `API_CLIENT_CERT` | — (unset) | PEM client certificate presented to the API, for mutual TLS (`API_TLS_CLIENT_CA` on the API, or a gateway that asks for one) |
| `API_CLIENT_KEY` | — (unset) | PEM private key for `API_CLIENT_CERT`; omit when the key is in the same file. Unencrypted only |
| `MCP_TRANSPORT` | `stdio` | `stdio`, `http`, or `sse` |
| `MCP_SERVER_HOST` | `localhost` | Bind host for HTTP/SSE |
| `MCP_SERVER_PORT` | `9000` | Bind port for HTTP/SSE |
| `MCP_BEHIND_PROXY` | `false` | Acknowledges an ingress in front of the HTTP transport that terminates TLS **and** authenticates callers — both, not either. Silences the exposure warning only — it grants the server no capability and changes no behavior |
| `MCP_STATELESS_HTTP` | `true` | Run the HTTP transport without a per-connection MCP session (no `Mcp-Session-Id`, no stream resumability) so instances scale without session affinity. Ignored for stdio; forced off for `sse` |
| `LOG_LEVEL` | `INFO` | Logging level |
| `API_TIMEOUT` | `30` | HTTP timeout in seconds |
## Tools
### Model lifecycle
| MCP Tool | Description |
| ------------------------------- | ---------------------------------------------------------------- |
| `get_obml_reference()` | Returns the full OBML format specification |
| `load_model(model? \| osi_yaml?, dedup=True)` | Parse, validate, and store a model (returns health + model_load). Pass `model` (OBML JSON) **or** `osi_yaml` (OSI YAML, converted to OBML server-side) |
| `validate_model(model? \| model_yaml?, extends?, inherits?, online=False, dialect?)` | Validate a model **without loading it**. `online=True` additionally probes the configured datasource for every declared table and column, reporting drift a structural check cannot see (`DATASOURCE_*`). `extends` / `inherits` are multi-model only — the stateless route single-model mode uses ignores both, so they are refused there rather than silently dropped |
| `describe_model(model_id)` | Inspect data objects, dimensions, measures, metrics |
| `remove_model(model_id)` | Remove a model from the current session |
| `list_models()` | List all models loaded in the current session |
| `export_model_to_osi(model_id, ...)` | Export a loaded model as OSI YAML |
### Model discovery
| MCP Tool | Description |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `find_artefacts(model_id, query?, kind?, name?)` | Look up artefacts. With `query` → **fuzzy, ranked search** (resolve a vague term: exact / synonym / fuzzy). Without `query` → **exact, deterministic lookup** (all artefacts, one kind, or one named artefact, full records) |
| `explain_artefact(model_id, name)` | Explain lineage of a dimension, measure, or metric |
| `list_examples(model_id, intent?)` | List authored example queries (filterable by intent tag) |
| `get_example(model_id, name)` | Get one example with query + compiled SQL preview |
| `get_join_graph(model_id)` | Return the join graph as an adjacency list |
| `find_composables(query_json?, anchors?, anchor_type?, model_id?)` | **ACR** — given an in-progress query or named anchor(s), return the dimensions/measures/metrics that still compose into a valid, fanout-free result (plus CFL candidates). Guaranteed to compile |
### Query, execution & diagrams
| MCP Tool | Description |
| ----------------------------- | ------------------------------------------------------- |
| `execute_query(...)` | Compile and execute a QueryObject, returning SQL + rows |
| `run_batch(queries, ...)` | One-shot: load a model + run N queries in parallel |
| `get_model_diagram(model_id)` | Generate a Mermaid ER diagram for a loaded model |
### Semantic graph (RDF / SPARQL)
| MCP Tool | Description |
| --------------------------------------------- | ------------------------------------------- |
| `get_model_graph(model_id)` | Return the model as OBSL-Core RDF (Turtle) |
| `query_model_graph_by_sparql(query, ...)` | Run a read-only SPARQL query (SELECT / ASK) |
### References
| MCP Tool | Description |
| ------------------------- | ------------------------------------------------------- |
| `get_obml_reference()` | OBML (model authoring) grammar reference |
| `get_function_catalog()` | Portable scalar functions usable in OBML expressions, with their pinned cross-dialect semantics |
| `get_json_schema(name)` | JSON Schema for `obml` (model) or `query` (QueryObject) |
### Utilities
| MCP Tool | Description |
| ----------------- | -------------------------------------------- |
| `list_dialects()` | List available SQL dialects and capabilities |
## Design-time vs run-time tool switching
The server presents a **phase-scoped tool surface**: instead of listing all
all tools at once, it shows only the tools that make sense for where you are in
the model lifecycle. About half the tools are meaningless until a model is
loaded (`execute_query`, `describe_model`, `find_artefacts`, …) and the rest are
about authoring or reference (`get_obml_reference`, `get_function_catalog`,
`get_json_schema`, `validate_model`, `list_dialects`). Splitting them keeps the surface small and prevents a
whole class of error — calling a query tool with no model loaded.
### Three buckets, swapped by phase
Tools fall into three buckets. The visible surface is a **swap** at the
load/unload transition, not additive — the run phase does **not** show the
design/reference tools:
| Bucket | Listed when | Tools |
| --------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Always** | always (both phases) | `load_model`, `remove_model` (transition verbs — stay available in the run phase so a second model can be loaded mid-session, up to `max_models_per_session`); `run_batch` and `validate_model` (self-contained — each takes its model inline, so neither needs prior session state); `get_json_schema` (QueryObject/OBML schemas — needed in both phases) |
| **Design-only** | only when no model loaded | `get_obml_reference`, `get_function_catalog`, `list_dialects` |
| **Run-only** | only when a model is loaded | `describe_model`, `get_model_diagram`, `find_artefacts`, `explain_artefact`, `execute_query`, `list_examples`, `get_example`, `get_model_graph`, `get_join_graph`, `find_composables`, `query_model_graph_by_sparql`, `list_models`, `export_model_to_osi` |
```
load_model (returns "re-list" signal)
┌─────────────────┐ ────────────────────────────────▶ ┌───────────────┐
│ design phase │ │ run phase │
│ always + design │ ◀─────────────────────────────── │ always + run │
└─────────────────┘ remove_model (last model) / TTL └───────────────┘
expiry — back to design phase
```
So **design phase → always + design-only**, **run phase → always + run-only**.
Design/reference tools are hidden once a model is loaded, keeping the run
surface focused on querying.
### Re-listing
The MCP `tools/list` response is filtered to the active phase. Because the
stateless MCP spec makes push notifications (`notifications/tools/list_changed`)
unreliable, transitions are **pull-based**: `load_model` (design → run) and
`remove_model` (run → design, once no models remain) return a short signal
telling the client to **re-list its tools** and pick up the swapped surface.
### Guard against premature calls
If a client calls a run-only verb while still in the design phase (e.g. a stale
host that hasn't re-listed yet), the server returns a **structured error**
rather than an opaque failure:
> No model loaded — '`execute_query`' is a run-time tool and is not available
> yet. Call `load_model` first, then re-list tools.
### Capability gating (orthogonal to phase)
Separately from lifecycle phase, a tool can be hidden because the server is
_configured_ not to support it. The execution tool `execute_query` is gated on
the API's `query_execute` capability: when the server runs **compile-only** it is
dropped from `tools/list` and calling it returns a structured error. This
composes with phase — a verb is listed only if its **phase is active _and_ its
capability is enabled**. The mechanism is a general capability registry, so
future "the server can't do X here" flags hide their tools the same way.
### Single-model mode
When the API runs in **single-model mode** a model is pre-loaded at startup, so
the server is permanently in the run-time phase — every applicable tool is
listed from the first request and there is no `load_model` step.
> **Note on caching hints.** The `2026-07-28` MCP spec adds `ttlMs` / `cacheScope`
> hints on `tools/list` (SEP-2549). These are intentionally **not** set yet — the
> fields are a release candidate, and FastMCP's list-tools hook exposes only the
> tool list, not the result envelope. The explicit re-list signal above is the
> primary (and spec-recommended) transition mechanism in the meantime.
## Supported SQL Dialects
`postgres`, `snowflake`, `clickhouse`, `databricks`, `dremio`, `bigquery`, `duckdb`
## Workflow
1. **Get reference** — call `get_obml_reference()` to learn OBML syntax
2. **Load model** — call `load_model(model_yaml)` to get a `model_id`
3. **Explore** — call `describe_model(model_id)` or use discovery tools (`find_artefacts`, `explain_artefact`)
4. **Execute** — call `execute_query(model_id, query_json='{"select": {"dimensions": [...], "measures": [...]}}')` to compile and run SQL, returning rows (requires `QUERY_EXECUTE=true` on the API; see `get_json_schema("query")` for the QueryObject shape)
## Integration Guides
Use the OrionBelt® Semantic Layer MCP server with popular AI agent frameworks and automation platforms:
| Framework | Transport | Guide |
| --------------------- | ---------------- | -------------------------------------------------------------------------------- |
| **OpenAI Agents SDK** | stdio, HTTP, SSE | [docs/integrations/openai-agents-sdk.md](docs/integrations/openai-agents-sdk.md) |
| **LangChain** | stdio, HTTP | [docs/integrations/langchain.md](docs/integrations/langchain.md) |
| **Google ADK** | stdio, HTTP, SSE | [docs/integrations/google-adk.md](docs/integrations/google-adk.md) |
| **n8n** | HTTP, SSE | [docs/integrations/n8n.md](docs/integrations/n8n.md) |
| **CrewAI** | stdio, HTTP | [docs/integrations/crewai.md](docs/integrations/crewai.md) |
Each guide includes quick-start examples, multi-agent patterns, and connection options for both the hosted demo and self-hosted deployments.
## Development
```bash
# Run tests
uv run pytest
# Lint and format
uv run ruff check server.py
uv run ruff format server.py tests/
# Set up pre-commit hooks (recommended)
./scripts/setup-hooks.sh
```
### GitHub Actions pinning
Every `uses:` in `.github/workflows` is pinned to a 40-character commit SHA with
a comment naming the exact patch release it was cut from:
```yaml
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
```
A tag such as `v7` is a movable label — the action's owner can repoint it at any
time, so `@v7` runs whatever commit it happens to name when the job starts. A
SHA cannot move. Every workflow also starts read-only (`permissions: contents:
read`), with write scopes granted on the single job that needs them (only
`id-token: write`, on the PyPI publish job).
Pinning makes each reference unreadable, though, and nothing inherently keeps a
SHA and the comment beside it in agreement: a pull request could swap the hash
for one taken from a fork, leave `# v7.0.1` untouched, and the diff would look
like a routine dependency bump. `scripts/check-action-pins.sh` closes that gap.
It requires a real SHA, an owner on its `ALLOWED_OWNERS` allowlist, and a
version comment naming an exact patch release, then resolves that tag upstream
with `git ls-remote` and fails if the commit it names is not the one pinned.
```bash
./scripts/check-action-pins.sh # every check, including upstream lookups
./scripts/check-action-pins.sh --offline # skip the network; SHA and comment format only
```
It runs as the `pins` job in CI, and again as the first step after checkout in
both tag-triggered publish workflows, so no release ships artifacts built by
steps whose pins were never verified. Bumping an action means updating the SHA
and its comment together; add a new action's owner to `ALLOWED_OWNERS`
deliberately, since a pin verifies against its own tag no matter who published
it.
### Release Process
The release script (`scripts/release.sh`) includes comprehensive pre-flight checks to prevent issues like the v2.8.2 formatting problem:
- **Code formatting check** - Ensures `ruff format` passes
- **Linting check** - Ensures `ruff check` passes
- **CI status check** - Warns if CI is not green
- **Test suite** - Runs all tests
- **Version consistency** - Verifies version across files
- **Changelog** - Ensures changelog entry exists
Pre-commit hooks are available to catch issues early. Run `./scripts/setup-hooks.sh` to install them.
## Hosted MCP Server
A public hosted instance of this MCP server runs on Google Cloud Run, connected
to the live OrionBelt® Semantic Layer demo API. No local install, no API key.
### Endpoint
```
https://orionbelt.ralforion.com/mcp
```
Streamable HTTP (MCP spec 2025-03-26). Stateful — clients should send the
`initialize` handshake and reuse the returned `Mcp-Session-Id` header.
### Quick start with Claude Desktop
Claude Desktop's config schema accepts only stdio launchers — for a remote
MCP server, use the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote)
stdio↔HTTP bridge (auto-fetched by `npx`, no manual install).
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
or `%APPDATA%\Claude\claude_desktop_config.json` (Windows) and add:
```json
{
"mcpServers": {
"orionbelt": {
"command": "npx",
"args": [
"mcp-remote",
"https://orionbelt.ralforion.com/mcp",
"--transport",
"http"
]
}
}
}
```
Fully quit Claude Desktop (⌘Q on macOS — closing the window isn't enough) and
reopen. The OrionBelt® tools then appear in the tools menu.
Alternatively, in newer Claude Desktop builds: **Settings → Connectors → Add
custom connector**, paste the URL above. No file editing or `npx` required.
> **Why `mcp-remote`?** Claude Desktop's `claude_desktop_config.json` schema
> currently only validates stdio entries (`command` + `args`). A bare
> `{"url": "…"}` entry is rejected with _"not valid MCP server configurations
> and were skipped"_. `mcp-remote` runs a local stdio bridge that forwards to
> the HTTPS endpoint, so Claude Desktop sees a normal stdio server. **Claude
> Code** does support `{"type": "url", "url": "…"}` natively — see below.
### Quick start with Claude Code
Add to `.mcp.json` in any repo (or `~/.config/claude-code/.mcp.json` globally):
```json
{
"mcpServers": {
"orionbelt": {
"type": "url",
"url": "https://orionbelt.ralforion.com/mcp"
}
}
}
```
### Other MCP clients
Any client that supports Streamable HTTP transport (MCP spec 2025-03-26) can
point at the URL above. The endpoint accepts `POST /mcp` with
`Accept: application/json, text/event-stream`. See
[`tests/cloudrun/test_mcp_cloudrun.sh`](tests/cloudrun/test_mcp_cloudrun.sh)
for a stdlib-only Python smoke test that walks the full handshake.
### Notes
- The hosted instance scales to zero when idle, so the first request after a
cold period takes ~1–2 seconds longer.
- It connects to the public demo API at `https://orionbelt.ralforion.com` — same data,
same dialects, no authentication. Don't load production data through it.
- For self-hosting, see the [Installation](#installation) section above and
the [`Dockerfile`](Dockerfile).
## License
Copyright 2026 [RALFORION d.o.o.](https://ralforion.com)
Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details.
### Third-party licenses
The PyPI package ships only `server.py` — its dependencies are declared, not
bundled, so it redistributes no third-party code.
The Docker image does bundle them, and carries their licenses in three places:
| In the image | Covers |
|---|---|
| `/app/.venv/lib/*/site-packages/*.dist-info/licenses/` | each Python package, as its author shipped it |
| `/app/THIRD_PARTY_NOTICES.md` | the same texts aggregated into one file |
| `/usr/share/doc/*/copyright` | the Debian base packages inherited from `python:3.14-slim` |
The aggregate is committed as
[`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md) and copied into the image,
so it is readable without pulling anything and is reviewed in the same diff as
the dependency change that altered it. CI regenerates it from the locked set on
every PR and fails if the committed copy differs, which is what keeps it from
drifting. To read the one an image actually carries — the ground truth for any
given tag:
```bash
docker run --rm --entrypoint cat ralforion/orionbelt-semantic-layer-mcp:latest \
/app/THIRD_PARTY_NOTICES.md
```
Regenerate it with `scripts/third_party_notices.py`, on Linux. It resolves for
the image's target rather than for your host, and a host-resolved run on macOS
would silently omit `jeepney` and `SecretStorage`.
**The Python dependencies are permissive with two exceptions**, both recorded
in `ACKNOWLEDGED` in `scripts/third_party_notices.py` rather than left to a
reader's inference: `certifi` is MPL-2.0, and `docutils` offers a choice that
includes the GPL. MPL-2.0 is file-level copyleft and reaches the files
themselves, not the program importing them, so shipping it unmodified alongside
this notice satisfies it. The `docutils` choice has to be made in writing before
a release, and the generator fails until it is. Everything else is MIT, BSD,
Apache-2.0, ISC, Unlicense or PSF-2.0, and none of it reaches the server's own
code.
**The Debian base is a separate matter.** `python:3.14-slim` brings ~87 system
packages, many of them GPL/LGPL (`coreutils`, `libc6`, `bash`, …), exactly as
every Debian-based image does. They are unmodified upstream packages invoked as
ordinary OS components, not linked into or derived from this server, and their
licenses and written-offer text ship in the image under `/usr/share/doc/` and
`/usr/share/common-licenses/`. If your distribution policy needs to avoid them
entirely, rebuild `FROM` a distroless or Alpine base.
---
<p align="center">
<a href="https://ralforion.com">
<img src="https://raw.githubusercontent.com/ralforion/orionbelt-semantic-layer-mcp/main/docs/assets/RALFORION_doo_Logo.png" alt="RALFORION d.o.o." width="200">
</a>
</p>
<p align="center">
Copyright © 2026 RALFORION d.o.o.
<br>
OrionBelt® is a registered trademark of RALFORION d.o.o.
</p>
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues