← Files Dataiku HeadlessARCHIVED FILE

scripts/build_http_plugin.py

5.41 KB · Nov 3, 2026 · 19:02 UTC

↓ Download file

#!/usr/bin/env python3

# Copyright 2026 Dataiku SAS
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""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()

SHA-256: bdf423a496c2a19bdcfc5fa78bc6e3f1e22bbb35f2eae6393a41b2783e1633c9