Metadata-Version: 2.4
Name: orionbelt-semantic-layer-mcp
Version: 2.7.9
Summary: Thin MCP server for OrionBelt Semantic Layer — delegates to REST API
Project-URL: Homepage, https://github.com/ralfbecher/orionbelt-semantic-layer-mcp
Project-URL: Repository, https://github.com/ralfbecher/orionbelt-semantic-layer-mcp
Project-URL: Issues, https://github.com/ralfbecher/orionbelt-semantic-layer-mcp/issues
Author-email: "Ralf Becher, RALFORION d.o.o." <ralf.becher@web.de>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: llm,mcp,orionbelt,semantic-layer
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.12
Requires-Dist: fastmcp<4,>=3.3
Requires-Dist: httpx<1,>=0.28
Requires-Dist: pydantic-settings<3,>=2.12
Description-Content-Type: text/markdown

<!-- mcp-name: io.github.ralfbecher/orionbelt-semantic-layer-mcp -->
<p align="center">
  <img src="https://raw.githubusercontent.com/ralfbecher/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>

[![Version 2.7.9](https://img.shields.io/badge/version-2.7.9-purple.svg)](https://github.com/ralfbecher/orionbelt-semantic-layer-mcp/releases)
[![OrionBelt Semantic Layer 2.7](https://img.shields.io/badge/OrionBelt_Semantic_Layer-2.7-0054A6.svg)](https://github.com/ralfbecher/orionbelt-semantic-layer)
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/ralfbecher/orionbelt-semantic-layer-mcp/blob/main/LICENSE)
[![FastMCP](https://img.shields.io/badge/FastMCP-3.3+-8A2BE2)](https://gofastmcp.com)
[![Pydantic v2](https://img.shields.io/badge/Pydantic-v2-E92063.svg?logo=pydantic&logoColor=white)](https://docs.pydantic.dev)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://docs.astral.sh/ruff/)

[![BigQuery](https://img.shields.io/badge/BigQuery-669DF6.svg?logo=googlebigquery&logoColor=white)](https://cloud.google.com/bigquery)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-4169E1.svg?logo=postgresql&logoColor=white)](https://www.postgresql.org)
[![Snowflake](https://img.shields.io/badge/Snowflake-29B5E8.svg?logo=snowflake&logoColor=white)](https://www.snowflake.com)
[![ClickHouse](https://img.shields.io/badge/ClickHouse-FFCC01.svg?logo=clickhouse&logoColor=black)](https://clickhouse.com)
[![Dremio](https://img.shields.io/badge/Dremio-31B48D.svg)](https://www.dremio.com)
[![Databricks](https://img.shields.io/badge/Databricks-FF3621.svg?logo=databricks&logoColor=white)](https://www.databricks.com)
[![DuckDB](https://img.shields.io/badge/DuckDB-FFF000.svg?logo=duckdb&logoColor=black)](https://duckdb.org)
[![MySQL](https://img.shields.io/badge/MySQL-4479A1.svg?logo=mysql&logoColor=white)](https://www.mysql.com)

A thin MCP server that delegates all business logic to the [OrionBelt Semantic Layer](https://github.com/ralfbecher/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/ralfbecher/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)
- **15 tools** (single-model mode) or **18 tools** (multi-model mode) for querying (QueryObject), execution, batch, discovery, examples, diagrams, RDF/SPARQL, and OBML reference + JSON schemas. The visible surface is smaller 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/ralfbecher/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
```

### 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 |
| `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                |
| `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, dedup=True)` | Parse, validate, and store a model (returns health + model_load) |
| `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                    |

### Model discovery

| MCP Tool                                 | Description                                                                                      |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `list_artefacts(model_id, kind?, name?)` | **Exact, deterministic lookup** — all artefacts, one kind, or one named artefact (full records)  |
| `find_artefacts(model_id, query, kind?)` | **Fuzzy, ranked search** — resolve a vague term to real artefact names (exact / synonym / fuzzy) |
| `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                                                       |

### 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_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`, `list_artefacts`, …) and the rest are
about authoring or reference (`get_obml_reference`, `get_json_schema`,
`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` (self-contained one-shot — loads/references a model inline, so it needs no prior session state)                        |
| **Design-only** | only when no model loaded   | `get_obml_reference`, `get_json_schema`, `list_dialects`                                                                                                                                  |
| **Run-only**    | only when a model is loaded | `describe_model`, `get_model_diagram`, `list_artefacts`, `find_artefacts`, `explain_artefact`, `execute_query`, `list_examples`, `get_example`, `get_model_graph`, `get_join_graph`, `query_model_graph_by_sparql`, `list_models` |

```
                       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 (`list_artefacts`, `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
uv run ruff check server.py
uv run ruff format server.py tests/
```

## 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 2025 [RALFORION d.o.o.](https://ralforion.com)

Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details.

---

<p align="center">
  <a href="https://ralforion.com">
    <img src="https://raw.githubusercontent.com/ralfbecher/orionbelt-semantic-layer-mcp/main/docs/assets/RALFORION_doo_Logo.png" alt="RALFORION d.o.o." width="200">
  </a>
</p>
