Skip to main content
Glama
README.md
# dss-mcp-bridge

Operate [Dataiku DSS](https://www.dataiku.com/) from any MCP client — Claude Code, Codex CLI,
Cursor — over **Streamable HTTP**.

![dss-mcp-bridge architecture](docs/diagrams/architecture.png)

It is **read-only by default**, authenticates with bearer tokens, validates `Origin` and `Host`
per the MCP specification, and records every tool call. What it exposes is a deliberate
configuration choice, not an accident of what happens to be implemented.

## Quick start

Not on PyPI yet — install from the repository:

```bash
git clone https://github.com/qsun-aidata/dss-mcp-bridge
cd dss-mcp-bridge
uv sync                                          # or: pip install .
```

```bash
cp .env.example .env && chmod 600 .env           # set DSS_MCP_DSS_API_KEY
uv run dss-mcp-bridge check                      # validate config + reach DSS
uv run dss-mcp-bridge serve                      # listens on 127.0.0.1:58000/mcp
```

Then connect your agent — the server prints the exact snippet for each one:

```bash
uv run dss-mcp-bridge client-config claude-code  # or codex | cursor | inspector
```

## Connecting an agent

<details>
<summary><b>Claude Code</b></summary>

```bash
claude mcp add --transport http dataiku http://127.0.0.1:58000/mcp \
  --header "Authorization: Bearer $DSS_MCP_TOKEN"
```

Or in `.mcp.json` (project scope, shared with your team — the token stays in the environment):

```json
{
  "mcpServers": {
    "dataiku": {
      "type": "http",
      "url": "http://127.0.0.1:58000/mcp",
      "headers": { "Authorization": "Bearer ${DSS_MCP_TOKEN}" }
    }
  }
}
```

Verify with `/mcp`.
</details>

<details>
<summary><b>Codex CLI</b></summary>

In `~/.codex/config.toml`:

```toml
[mcp_servers.dataiku]
url = "http://127.0.0.1:58000/mcp"
bearer_token_env_var = "DSS_MCP_TOKEN"
```

Codex reads the variable at connect time, so the token never lands in the config file.
</details>

<details>
<summary><b>Cursor</b></summary>

In `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):

```json
{
  "mcpServers": {
    "dataiku": {
      "url": "http://127.0.0.1:58000/mcp",
      "headers": { "Authorization": "Bearer ${env:DSS_MCP_TOKEN}" }
    }
  }
}
```
</details>

Clients that support OAuth discovery find the server through the RFC 9728 metadata it
publishes at `/.well-known/oauth-protected-resource/mcp`.

![MCP tool call sequence](docs/diagrams/tool-call-sequence.png)

## Permission tiers

`DSS_MCP_MODE` decides which tools **exist**. A tool above the active tier is never registered,
so it does not appear in `tools/list` and a model cannot try to call it.

| Mode | Adds | Use for |
|---|---|---|
| `read` *(default)* | list, get, search | exploration, analysis, RAG |
| `write` | create, duplicate, properties, export, upload, dataset writes | building projects |
| `admin` | `delete_project`, `set_project_permissions` | administration |

`list_dku_tools` reports the active mode, everything being withheld, and why — so an agent
can explain the limitation instead of guessing.

## Tools

<!-- TOOL-TABLE:START -->
| Tool | Tier | Description |
|---|---|---|
| `create_managed_folder` | write | Create an empty managed folder in a project. |
| `create_project` | write | Create a project. Returns created=false if the key is already taken. |
| `delete_project` | admin | Permanently delete a project, its managed datasets and its output folders. |
| `dku_call` | read | Generic Dataiku API bridge (allowlisted read-only methods). |
| `duplicate_project` | write | Duplicate a project into a new project key. |
| `export_project` | write | Export a project to a zip archive on the MCP server host. |
| `get_dataset` | read | Get the type, schema columns and parameters of one dataset. |
| `get_project` | read | Get the name, owner, description, tags and status of one project. |
| `get_project_permissions` | read | List the group permissions set on a project. |
| `list_datasets` | read | List the datasets in a project. |
| `list_dku_tools` | read | List the tools this MCP server exposes, and what it is withholding. |
| `list_projects` | read | List every DSS project key visible to this server's API key. |
| `search_dataset` | read | Search a dataset's rows. |
| `set_project_permissions` | admin | Add or replace one group's permissions on a project. |
| `set_project_properties` | write | Update a project's description, tags and/or status. |
| `upload_file` | write | Upload a base64-encoded file into a project's managed folder. |
| `write_dataset_records` | write | *the in-process `dataiku` package is not importable* |
<!-- TOOL-TABLE:END -->

`search_dataset` streams rows over the REST API with a scan cap; `text_query` matches any
string column and `filters` add per-column conditions combined with AND.

**`dku_call`** reaches arbitrary methods of the `dataikuapi` surface. It ships **disabled**.
`DSS_MCP_DKU_CALL=allowlist` permits only the read-only names in
[`dku_call_allowlist.yaml`](src/dss_mcp_bridge/tools/dku_call_allowlist.yaml); `full` removes
that restriction and additionally requires `DSS_MCP_MODE=admin`. In every mode, attributes
whose name starts with `_` are refused, so dunder traversal is never reachable, and the
allowlist gates the accessors used to serialize a result as well as the method you name.

The shipped allowlist covers project and dataset analysis. Instance administration —
connections, users, groups, plugin presets — is left out on purpose, because `allowlist` mode
registers `dku_call` at the **read** tier. Copy the file and point
`DSS_MCP_DKU_CALL_ALLOWLIST_FILE` at your copy to widen it.

### DSS Agent tools

Set `DSS_MCP_AGENT_TOOLS=PROJECT_KEY:TOOL_ID,…` and each one is registered as an MCP tool
with a validated schema built from its DSS descriptor. A descriptor that cannot be loaded, or
declares a parameter name Python cannot use, is reported in `list_dku_tools` rather than
silently dropped.

## Optional: in-process `dataiku`

The DSS REST API cannot write dataset rows, so `write_dataset_records` needs the in-process
`dataiku` package — which ships with the DSS distribution, not PyPI. Run the bridge on a host
that has it and put the DSS `python` directory on `PYTHONPATH`:

```bash
PYTHONPATH=/opt/dataiku-dss/dataiku-dss-14.7.3/python dss-mcp-bridge serve
```

Everything else works over REST from any host that can reach `DSS_URL`. When `dataiku` is
missing, the affected tools are withheld and `list_dku_tools` says so.

## Configuration

Full list with comments in [`.env.example`](.env.example); `dss-mcp-bridge print-config`
shows the effective values with secrets redacted. The essentials:

| Variable | Default | Description |
|---|---|---|
| `DSS_MCP_DSS_URL` | `http://127.0.0.1:11000` | DSS base URL |
| `DSS_MCP_DSS_API_KEY` | *(required)* | DSS API key — treat as admin |
| `DSS_MCP_HOST` / `DSS_MCP_PORT` | `127.0.0.1` / `58000` | Listen address |
| `DSS_MCP_PUBLIC_URL` | — | URL clients use; widens the Host allowlist |
| `DSS_MCP_MODE` | `read` | `read` \| `write` \| `admin` |
| `DSS_MCP_DKU_CALL` | `off` | `off` \| `allowlist` \| `full` |
| `DSS_MCP_TOKEN` / `DSS_MCP_TOKEN_FILE` | — | Bearer token(s) |
| `DSS_MCP_AGENT_TOOLS` | — | `PROJECT_KEY:TOOL_ID,…` |
| `DSS_MCP_AUDIT_FILE` | *(stderr)* | Rotating JSON-lines audit log |

The original script's names (`DSS_URL`, `MCP_PORT`, `MCP_ACCESS_TOKEN`, …) still work and
log a deprecation warning. They will be removed in 1.0.

## Deployment

### Docker

```bash
docker build -t dss-mcp-bridge .
docker run --rm -p 58000:58000 --env-file .env \
  -e DSS_MCP_HOST=0.0.0.0 \
  -e DSS_MCP_PUBLIC_URL=http://localhost:58000/mcp \
  dss-mcp-bridge
```

### systemd (user unit)

```bash
uv sync
cp .env.example .env && chmod 600 .env
./scripts/install-service.sh
journalctl --user -u dss-mcp-bridge -f
```

### Remote DSS, local agent

The recommended layout is the bridge on the DSS host bound to loopback, reached through an
SSH tunnel — no open port, no LAN exposure, and `127.0.0.1` stays a valid `Host`:

```bash
ssh -N -L 58000:127.0.0.1:58000 you@dss-host
```

To expose the port directly instead, set `DSS_MCP_TOKEN` (the server refuses to bind a
non-loopback address without one) and `DSS_MCP_PUBLIC_URL`, and terminate TLS in front.

## Security

Full model in [`docs/security.md`](docs/security.md); [`SECURITY.md`](SECURITY.md) covers
reporting. In short:

- **MCP endpoint access ≈ the privileges of `DSS_MCP_DSS_API_KEY`.** Treat the endpoint as
  you would that key.
- Loopback bind by default; **binding anything else without a token is refused at startup**.
- Bearer tokens are compared as SHA-256 digests in constant time, are never logged, and can
  be rotated (several at once) while staying individually attributable in the audit log.
- `Origin` and `Host` are validated on every request (403 / 421), as the MCP spec requires.
- `export_project` cannot write outside `DSS_MCP_EXPORT_DIR`; `upload_file` rejects
  path-like names.
- The DSS API key and bearer tokens are scrubbed from errors, logs and audit records.

Verify a running deployment:

```bash
./scripts/smoke.sh http://127.0.0.1:58000/mcp "$DSS_MCP_TOKEN"
```

## Development

```bash
uv sync --all-extras
uv run pytest              # tests + coverage gate
uv run ruff format --check src tests scripts
uv run ruff check src tests scripts
uv run mypy
python scripts/gen_tool_table.py --check   # README table matches the code
```

## Related projects

| Project | Role |
|---|---|
| [**dss-ollama-mesh**](https://github.com/qsun-aidata/dss-ollama-mesh) | Unrelated — Ollama plugin for DSS LLM Mesh |

## Author

**Qian SUN** — [contact@qsun.fr](mailto:contact@qsun.fr)

Licensed under [Apache License 2.0](LICENSE).