Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .dataiku/http-config.json.entra-example
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
},
"dss_instances": {
"prod": {
"url": "https://dss.example",
"delegated_scope": "api://replace-with-dss-app-client-id/dss.access"
"url": "https://dataiku.example",
"delegated_scope": "api://replace-with-dataiku-app-client-id/dataiku.access"
}
},
"user_selections": {}
Expand Down
6 changes: 3 additions & 3 deletions .dataiku/http-config.json.generic_oidc-example
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
},
"dss_instances": {
"prod": {
"url": "https://dss.example",
"delegated_audience": "dss-prod",
"delegated_scope": "dss.api"
"url": "https://dataiku.example",
"delegated_audience": "dataiku-prod",
"delegated_scope": "dataiku.api"
}
},
"user_selections": {}
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ migration_token_diet.md
sample_extracted/
extract/
*_extracted/

# Default http plugin bundle location
dataiku-headless-http/
dataiku-headless-http.zip
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,18 @@

Dataiku Headless is an MCP server with tools for working in Dataiku, plus skills that teach AI assistants how to use them. Connect it to a Dataiku instance, and your AI assistant can build data pipelines, models, dashboards, agents, and more.

Install it from the [Claude Code](#claude-code-cli) or [Codex](#codex-cli) plugin marketplace, or install it as an agent plugin from this GitHub repository for Cursor, Snowflake CoCo, AWS Kiro, OpenCode, and more.
Dataiku Headless supports two connection modes:

| Mode | MCP server | Authentication | Installation |
| --- | --- | --- | --- |
| Local stdio | Runs on the user's workstation | Personal Dataiku API key | Install the local plugin |
| Customer-managed HTTP | Runs as an organization-managed service | Enterprise OAuth and delegated Dataiku identity | Install the customer-specific remote plugin distributed by the administrator |

Do not enable both Dataiku MCP definitions in the same client. They expose the same tools with different credential ownership and can cause the agent to target the wrong server.

The rest of this README covers the Dataiku Headless marketplace plugin, which uses stdio transport. For customer-managed HTTP installation, endpoint distribution, OAuth login, and end-user verification, see [Streamable HTTP deployment](docs/http-deployment.md#distribute-the-interactive-oauth-plugin).

Install the plugin from the [Claude Code](#claude-code-cli) or [Codex](#codex-cli) plugin marketplace, or install it as an agent plugin from this GitHub repository for Cursor, Snowflake CoCo, AWS Kiro, OpenCode, and more.

## Requirements

Expand Down Expand Up @@ -151,7 +162,7 @@ The reference library covers the main Dataiku object areas and workflows, includ

## Stdio onboarding and authentication

The onboarding flow is the same:
The onboarding flow is:

1. Ask the agent to **Set up Dataiku Headless** (or run `/dataiku-headless:dataiku-headless-setup` in Claude Code).
2. Approve the MCP URL prompt.
Expand Down Expand Up @@ -273,7 +284,7 @@ uv run --quiet --locked --script ./runtime/run_mcp.py --transport stdio
│ ├── run_mcp.py # Server entry point: PEP 723 script pinning the runtime deps inline
│ └── run_mcp.py.lock # Committed, full dependency resolution for the entry point
├── .claude-plugin/
│ ├── plugin.json # Claude Code plugin manifest (skills + unconfigured stdio MCP)
│ ├── plugin.json # Claude Code plugin manifest (skills + stdio MCP)
│ └── marketplace.json # Marketplace catalog (single-plugin, source: "./")
├── .codex-plugin/
│ └── plugin.json # Codex plugin manifest
Expand Down
203 changes: 101 additions & 102 deletions docs/http-deployment.md

Large diffs are not rendered by default.

143 changes: 143 additions & 0 deletions scripts/build_http_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Render the HTTP Dataiku Headless plugin variant from the stdio source tree."""

from __future__ import annotations

import argparse
import json
import shutil
import tempfile
from pathlib import Path
from urllib.parse import urlsplit


ROOT = Path(__file__).resolve().parents[1]
ASSETS = ROOT / "scripts" / "http_plugin_assets"
PLUGIN_NAME = "dataiku-headless-http"
COPY_PATHS = (
"LICENSE",
"skills",
"docs/assets",
".mcp.json",
".codex-plugin",
".claude-plugin",
)


def validate_url(value: str) -> str:
"""Return a safe, absolute Streamable HTTP endpoint."""
endpoint = value.strip().rstrip("/")
parsed = urlsplit(endpoint)
if parsed.scheme != "https" or not parsed.netloc:
raise ValueError("The MCP URL must be an absolute HTTPS URL.")
if parsed.username or parsed.password:
raise ValueError("The MCP URL must not contain credentials.")
if parsed.query or parsed.fragment:
raise ValueError("The MCP URL must not contain a query or fragment.")
return endpoint


def read_json(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))


def write_json(path: Path, value: dict) -> None:
path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")


def update_manifests(destination: Path, *, endpoint: str) -> None:
"""Apply the HTTP-specific differences to copied source manifests."""
mcp = read_json(destination / ".mcp.json")
mcp["mcpServers"]["dataiku"] = {"type": "http", "url": endpoint}
write_json(destination / ".mcp.json", mcp)

codex = read_json(destination / ".codex-plugin" / "plugin.json")
codex["name"] = PLUGIN_NAME
codex["interface"]["displayName"] = "Dataiku Headless (HTTP)"
write_json(destination / ".codex-plugin" / "plugin.json", codex)

claude = read_json(destination / ".claude-plugin" / "plugin.json")
claude["name"] = PLUGIN_NAME
claude["displayName"] = "Dataiku Headless (HTTP)"
claude["mcpServers"]["dataiku"] = {"type": "http", "url": endpoint}
write_json(destination / ".claude-plugin" / "plugin.json", claude)

marketplace = read_json(destination / ".claude-plugin" / "marketplace.json")
marketplace["plugins"][0]["name"] = PLUGIN_NAME
write_json(destination / ".claude-plugin" / "marketplace.json", marketplace)


def populate_plugin(destination: Path, *, endpoint: str) -> None:
"""Copy shared plugin content and overlay the HTTP-specific assets."""
destination.mkdir(parents=True)
for relative_path in COPY_PATHS:
source = ROOT / relative_path
target = destination / relative_path
if source.is_dir():
shutil.copytree(source, target)
else:
shutil.copy2(source, target)

shutil.rmtree(destination / "skills" / "dataiku-headless-setup")
shutil.copytree(
ASSETS / "skills" / "dataiku-headless-setup",
destination / "skills" / "dataiku-headless-setup",
)

update_manifests(destination, endpoint=endpoint)


def render(output: Path, *, endpoint: str, archive: bool) -> Path:
"""Render an HTTP plugin directory, or a ZIP containing that directory."""
endpoint = validate_url(endpoint)
output = output.expanduser().resolve()
artifact = output.with_suffix(".zip") if archive else output
if artifact.exists():
raise ValueError(f"Output path already exists: '{artifact}'.")
if output.exists() and archive:
raise ValueError(f"Output path already exists: '{output}'.")

artifact.parent.mkdir(parents=True, exist_ok=True)
temporary_root = Path(
tempfile.mkdtemp(prefix=f".{output.name}-", dir=artifact.parent)
)
staged_plugin = temporary_root / PLUGIN_NAME
try:
populate_plugin(staged_plugin, endpoint=endpoint)
if archive:
staged_archive = shutil.make_archive(
str(temporary_root / output.name), "zip", temporary_root, PLUGIN_NAME
)
Path(staged_archive).rename(artifact)
else:
staged_plugin.rename(artifact)
finally:
shutil.rmtree(temporary_root, ignore_errors=True)
return artifact


def parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Render the Dataiku Headless HTTP plugin variant."
)
parser.add_argument(
"--url", required=True, help="HTTPS Streamable HTTP MCP endpoint."
)
parser.add_argument("--output", type=Path, required=True, help="Artifact path.")
parser.add_argument(
"--zip", action="store_true", help="Write <output>.zip instead of a directory."
)
return parser


def main() -> None:
args = parser().parse_args()
try:
artifact = render(args.output, endpoint=args.url, archive=args.zip)
except ValueError as error:
parser().error(str(error))
print(f"Rendered {PLUGIN_NAME} at {artifact}")


if __name__ == "__main__":
main()
22 changes: 22 additions & 0 deletions scripts/http_plugin_assets/skills/dataiku-headless-setup/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
name: dataiku-headless-setup
description: Set up the organization-managed Dataiku Headless Streamable HTTP connection. Use when the user asks to install, connect, configure, or repair Dataiku Headless. Verify the remote MCP connection, complete OAuth when needed, then select and verify a Dataiku instance.
---

# Set Up Dataiku Headless

Use this workflow for the customer-managed Dataiku Headless Streamable HTTP service bundled with this plugin. The workstation does not run the server.

## Authenticate and verify access

1. Check whether the Dataiku MCP tools are available. If they are, run `list_instances`.
2. If the tools are unavailable, or `list_instances` reports that authentication is required, explain that the remote server needs OAuth login and ask whether the user wants to authenticate now.
3. Only after the user agrees, run the matching client command:
- Codex: `codex mcp login dataiku`
- Claude Code: `claude mcp login dataiku`
4. Have the user complete the browser-based sign-in. Reload or restart the client if the tools do not reconnect, then retry `list_instances`.
5. HTTP catalogs are administrator-managed. If no instance is active, ask the user which listed instance to use and call `switch_instance`. Never call `configure_instance` or `delete_instance`.
6. Call `get_current_instance` to verify delegated Dataiku access and capture the Dataiku version when available.
7. Call `list_projects` as a lightweight read-only permission check.

Report the active instance name and URL, Dataiku version when available, and whether the read check succeeded. For an OAuth, token-exchange, or Dataiku identity-mapping failure, report the stage and direct the user to the Dataiku Headless administrator without requesting credentials.
19 changes: 10 additions & 9 deletions skills/dataiku-headless-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ Bring a new or broken plugin installation to a verified Dataiku connection. A re

## Workflow

1. Check the runtime first. Run `uv --version` when local commands are available; ask the user to run it only when they are not. Dataiku Headless requires uv 0.12.0 or later.
2. If uv is missing or too old, explain briefly that it supplies the isolated Python runtime and pinned dependencies used by the local MCP server. Detect the operating system and offer the matching official Astral installer:
1. First run `list_instances`. If it succeeds, the local plugin and runtime are already available; skip directly to instance configuration and verification below (step 8).
2. If the Dataiku MCP tools are unavailable, check the runtime. Run `uv --version` when local commands are available; ask the user to run it only when they are not. Dataiku Headless requires uv 0.12.0 or later.
3. If uv is missing or too old, explain briefly that it supplies the isolated Python runtime and pinned dependencies used by the local MCP server. Detect the operating system and offer the matching official Astral installer:
- macOS or Linux: `curl -LsSf https://astral.sh/uv/install.sh | sh`
- Windows PowerShell: `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`
3. Obtain explicit approval, run only the selected installer, and verify with `uv --version`. Do not substitute a third-party package manager or edit shell startup files unless the user asks.
4. If the current agent process cannot see the newly installed executable, use the installer's reported location to confirm it exists, then ask the user to fully restart or reload the agent. Stop and resume setup in the new session; the already-running MCP process cannot repair its own launch environment.
5. Once uv is suitable, warm the runtime by running the server once with stdin closed. Locate the absolute path of this `SKILL.md`; the plugin root is its ancestor containing both `skills/` and `runtime/` (this file is at `<plugin_root>/skills/dataiku-headless-setup/SKILL.md`). Do not assume the current working directory is the plugin root.
4. Obtain explicit approval, run only the selected installer, and verify with `uv --version`. Do not substitute a third-party package manager or edit shell startup files unless the user asks.
5. If the current agent process cannot see the newly installed executable, use the installer's reported location to confirm it exists, then ask the user to fully restart or reload the agent. Stop and resume setup in the new session; the already-running MCP process cannot repair its own launch environment.
6. Once uv is suitable, warm the runtime by running the server once with stdin closed. Locate the absolute path of this `SKILL.md`; the plugin root is its ancestor containing both `skills/` and `runtime/` (this file is at `<plugin_root>/skills/dataiku-headless-setup/SKILL.md`). Do not assume the current working directory is the plugin root.
- macOS or Linux: `uv run --quiet --locked --script "<plugin_root>/runtime/run_mcp.py" --transport stdio < /dev/null`
- Windows PowerShell: `$null | uv run --quiet --locked --script "<plugin_root>\runtime\run_mcp.py" --transport stdio`

A startup line on stderr followed by exit status 0 is expected. Do not substitute `uv sync --locked --script`: it caches downloads but leaves environment creation for the first server launch.
6. Check whether the Dataiku MCP tools are available. If they are not, reload the plugin or restart the agent once before diagnosing a Dataiku connection problem.
7. When the MCP tools are available, run `list_instances`. If no instance is configured, run `configure_instance` and have the user complete the local setup page. If multiple instances exist without an active one, ask which to use and run `switch_instance`.
8. Verify the active profile with `get_current_instance`. If its `connection_status` is `failed`, the discovered profile cannot connect to Dataiku; run `configure_instance` to replace or add a working profile instead of treating it as set up. When it is `connected`, make a lightweight read-only Dataiku call such as `list_projects` to validate the available Dataiku access. Never request or repeat the API key in chat.
7. Retry `list_instances`. If the tools are still unavailable, reload the plugin or restart the agent once, then retry before diagnosing a Dataiku connection problem.
8. When `list_instances` succeeds, if no instance is configured, run `configure_instance` and have the user complete the local setup page. If multiple instances exist without an active one, ask which to use and run `switch_instance`.
9. Verify the active profile with `get_current_instance`. If its `connection_status` is `failed`, the discovered profile cannot connect to Dataiku; run `configure_instance` to replace or add a working profile instead of treating it as set up. When it is `connected`, make a lightweight read-only Dataiku call such as `list_projects` to validate the available Dataiku access. Never request or repeat the API key in chat.

Report completion with the uv version, active instance name and URL, Dataiku version when available, and whether the Dataiku read succeeded. If a restart is required, say that setup is incomplete and give the single next action.
Report completion with the active instance name and URL, Dataiku version when available, and whether the Dataiku read succeeded. Include the uv version only when runtime recovery was needed. If a restart is required, say that setup is incomplete and give the single next action.
2 changes: 1 addition & 1 deletion skills/dataiku-headless/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Use this for any Dataiku task. Choose the right reference guide first, inspect t

## Shared Operating Rules

1. If the user asks to install, set up, connect, or repair Dataiku Headless, or its MCP tools are unavailable just after installation, read `../dataiku-headless-setup/SKILL.md` and follow it before continuing.
1. If the user asks to install, set up, connect, or repair Dataiku Headless, or its MCP tools are unavailable just after installation, read `../dataiku-headless-setup/SKILL.md`. Choose either local stdio or customer-managed HTTP setup and never enable both.
2. Ensure an instance is configured before any Dataiku work. In local stdio mode, if `get_current_instance` errors or `list_instances` is empty, run `configure_instance` first. In HTTP mode, use `list_instances` then `switch_instance`; the instance catalog is platform-managed. Keep the reported `dataiku_version` in context for version-sensitive requests.
3. Discover project keys and object identifiers through tools; do not invent them.
4. Read before write. Inspect the current object, flow context, jobs, or run history before changing anything.
Expand Down
2 changes: 1 addition & 1 deletion skills/dataiku-headless/references/cobuild.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Do not use this guide when:
- Answer confirmations only with the exact current `turn_id`. Old, duplicate, and mismatched turn IDs are rejected.
- Answer questions only with their exact current `turn_id` and an explicit `answers` list. Use `answers=[]` with `rejected=true` to decline.
- When `rejected=true`, `answers` must be empty.
- Cobuild conversations can continue concurrently in the Dataiku UI and through MCP/API. A question or deletion confirmation is a single DSS-side action; the first channel to answer consumes it.
- Cobuild conversations can continue concurrently in the Dataiku UI and through MCP/API. A question or deletion confirmation is a single Dataiku-side action; the first channel to answer consumes it.
- If an MCP answer returns "No pending question/confirmation found," it may have been answered in the UI or invalidated server-side. Do not retry; inspect the project or UI state, then continue the same conversation with a new message if appropriate.
- Answer a question only when the user request or inspected context determines the answer. Otherwise, ask the user.
- Honor `question.allow_multiple_answers` and `question.allow_custom_answer`; set `used_custom_answer=true` when supplying a custom free-text answer.
Expand Down
8 changes: 4 additions & 4 deletions tests/test_http_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,9 @@ def test_generic_oidc_http_config_example_is_valid(monkeypatch):
instances, selections = http.get_instances_and_selections()
assert isinstance(http._load_config(), HTTPConfig)
assert set(instances) == {"prod"}
assert instances["prod"].url == "https://dss.example"
assert instances["prod"].delegated_audience == "dss-prod"
assert instances["prod"].delegated_scope == "dss.api"
assert instances["prod"].url == "https://dataiku.example"
assert instances["prod"].delegated_audience == "dataiku-prod"
assert instances["prod"].delegated_scope == "dataiku.api"
assert selections == {}


Expand All @@ -474,7 +474,7 @@ def test_entra_http_config_example_is_valid(monkeypatch):
instances, selections = http.get_instances_and_selections()
assert instances["prod"].delegated_audience == ""
assert instances["prod"].delegated_scope == (
"api://replace-with-dss-app-client-id/dss.access"
"api://replace-with-dataiku-app-client-id/dataiku.access"
)
assert selections == {}

Expand Down
Loading