diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a0a07c..df96418 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -189,35 +189,3 @@ jobs: env: STACKQL_MCP_BUNDLE: dist/stackql-mcp-linux-x64.mcpb run: python3 scripts/smoke-test.py --cmd "$PWD/.venv/bin/stackql-mcp" - - # GitHub Action: installs from a locally built bundle (published assets may - # not exist yet for a new version at PR time), then smokes the server using - # the action's own emitted mcp-config. - action-test: - name: setup action (smoke) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Build local bundle for the action to install - run: make one TARGET=linux-x64 VERSION=${{ inputs.version }} - - - id: setup - uses: ./action - with: - bundle-path: dist/stackql-mcp-linux-x64.mcpb - - - name: Smoke test via emitted config - env: - MCP_CONFIG: ${{ steps.setup.outputs.mcp-config }} - run: | - echo "$MCP_CONFIG" | python3 -m json.tool > /dev/null && echo "mcp-config is valid JSON" - python3 - <<'EOF' - import importlib.util, json, os - cfg = json.loads(os.environ["MCP_CONFIG"]) - server = cfg["mcpServers"]["stackql"] - spec = importlib.util.spec_from_file_location("smoke", "scripts/smoke-test.py") - smoke = importlib.util.module_from_spec(spec) - spec.loader.exec_module(smoke) - smoke.run_command([server["command"], *server["args"]]) - EOF diff --git a/.gitignore b/.gitignore index 76ad476..49eea2d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ pypi/dist/ pypi/src/stackql_mcp_server/platforms.json pypi/src/*.egg-info/ /.venv* +.pypirc +__pycache__/ diff --git a/README.md b/README.md index da294fe..321219f 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,8 @@ Two more vectors ship from the same pipeline: - **PyPI wrapper** (`stackql-mcp-server`): same launcher pattern in stdlib-only Python (uvx/pip), sharing the npm wrapper's binary cache. PR CI smoke-tests it; the publish workflow builds sdist+wheel as a run artifact. Publishing is manual (2FA): `make pypi-build VERSION=X.Y.Z` then `python -m twine upload pypi/dist/*`. The registry validates pypi packages via the `mcp-name:` marker in the README. - **npm wrapper** (`@stackql/mcp-server`): an npx-able launcher that downloads the platform's published `.mcpb` (sha256-verified against pins baked into the package) and spawns the binary. PR CI tests the wrapper against a locally built bundle; the publish workflow renders the real pins from the published assets and uploads the tarball as a run artifact. Publishing to npmjs is deliberately manual (2FA): download the artifact or run `make npm-pack VERSION=X.Y.Z` locally, then `cd npm && npm publish --access public`. The package carries the `mcpName` field the MCP Registry requires for npm package validation. +A GitHub Action consumes these published bundles for CI: **[stackql/setup-stackql-mcp](https://github.com/stackql/setup-stackql-mcp)** installs the signed binary (sha256-verified at runtime against the release checksums) and emits an `mcpServers` config for agentic workflows (e.g. `anthropics/claude-code-action`). It lives in its own repo and tracks no version - `version: latest` by default, pin with `version: X.Y.Z` - so this packaging repo no longer carries the action source. + Steps 3 and 4 of the local runbook below (MCP Registry publish and aggregator listings) are still manual after a CI publish. After the OCI image and npm package exist for a version, the registry `server.json` (which now includes oci and npm package entries) can be published - the registry validates the npm `mcpName` and the image label at publish time, so those artifacts must exist first. ## Release runbook (local fallback) @@ -319,6 +321,95 @@ make publish VERSION=X.Y.Z This is interactive (PIN prompt), so it is a local flow, not a CI step. Requires an OpenSSL build with the PKCS#11 engine (libp11) pointed at the token vendor's PKCS#11 module. `--strip-only` removes an existing signature block if you need the unsigned bytes back. +## Publishing the wrapper packages (npm, PyPI, OCI) + +Explicit command sequences for the three downstream package vectors. All are +manual publishes (2FA / tokens). The `make` targets are convenience wrappers; +the raw commands below are what they run, useful on machines without `make` +(e.g. Git Bash / WSL on Windows). Run every command FROM THE REPO ROOT unless +a `cd` says otherwise. + +Ordering rule for all three: the manifest/pin render steps fetch the canonical +`.sha256` files from the PUBLISHED GitHub release, so they must run AFTER the +`.mcpb` assets for the version are published. Never pin locally built hashes. + +### npm (`@stackql/mcp-server`) + +```bash +# auth: browser-based login; account must have publish rights on @stackql +npm login + +# render pins from the published release, install the one dep, dry-run to inspect +bash scripts/render-npm-manifest.sh --version X.Y.Z +cd npm && npm install --no-audit --no-fund +npm publish --dry-run --access public # confirm: 4 files, name, version, mcpName +npm publish --access public # the real publish (prompts for 2FA) +cd .. + +# verify from a clean cache (forces download + sha256 verify of the bundle) +rm -rf ~/.stackql/mcp-server-bin +python3 scripts/smoke-test.py --cmd "npx -y @stackql/mcp-server" +``` + +Note: the npm registry CDN can lag a few minutes after first publish - if +`npx` returns 404 immediately after publishing, confirm with +`npm view @stackql/mcp-server version` and retry once it reports the version. + +### PyPI (`stackql-mcp-server`) + +PyPI build/publish tooling is not stdlib, and modern Debian/Ubuntu (incl. WSL) +block `pip install` into the system Python (PEP 668), so use a venv: + +```bash +# one-time per machine: a venv holding build + twine + uv +python3 -m venv ~/.venvs/stackql-publish +source ~/.venvs/stackql-publish/bin/activate +pip install --upgrade build twine uv + +# render pins, build sdist+wheel, validate (run from repo root) +bash scripts/render-pypi-manifest.sh --version X.Y.Z +cd pypi && python3 -m build && python3 -m twine check dist/* + +# upload. Auth is a PyPI API token (username __token__, password pypi-...). +# pypi/.pypirc holds it (gitignored); twine needs to be pointed at it since it +# only auto-reads ~/.pypirc. From the pypi/ dir: +python3 -m twine upload --config-file .pypirc dist/* +# (interactive fallback if no .pypirc: python3 -m twine upload dist/* ) +cd .. + +# verify from a clean cache (uvx; keep the venv active so uv is on PATH) +rm -rf ~/.stackql/mcp-server-bin +python3 scripts/smoke-test.py --cmd "uvx stackql-mcp-server" +``` + +### OCI image (`docker.io/stackql/stackql-mcp`) + +Push locally - do NOT use the dispatch publish workflow for an existing release +(it re-clobbers the `.mcpb` bundles and invalidates every pin). Requires +`docker login` with push rights on `stackql/stackql-mcp`. + +```bash +docker login + +# stage the linux binaries into the build context (downloads zips if absent) +make oci-stage VERSION=X.Y.Z # or run the unzip steps the target wraps + +# multi-arch build + push +docker buildx create --use --name mcp-builder 2>/dev/null || docker buildx use mcp-builder +docker buildx build --platform linux/amd64,linux/arm64 --push \ + --build-arg VERSION=X.Y.Z \ + -f oci/Dockerfile \ + -t docker.io/stackql/stackql-mcp:X.Y.Z \ + -t docker.io/stackql/stackql-mcp:latest . + +# verify from the registry (not the local build cache) +docker pull stackql/stackql-mcp:X.Y.Z +python3 scripts/smoke-test.py --docker stackql/stackql-mcp:X.Y.Z +``` + +For future tagged releases, set the `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN` +repo secrets and the publish workflow pushes the image automatically. + ## Makefile reference ``` diff --git a/action/README.md b/action/README.md deleted file mode 100644 index de815de..0000000 --- a/action/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Setup StackQL MCP Server (GitHub Action) - -Installs the signed [StackQL](https://stackql.io) binary (sha256-verified -against the release checksums) and emits an `mcpServers` JSON config that -plugs straight into MCP-capable actions like -[anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) - -giving CI agents live SQL query (and optionally provisioning) access to AWS, -Azure, Google, GitHub, Databricks, and 40+ other providers. - -Defaults to `read_only` server mode - the safe default for agentic CI. - -## Inputs - -| Input | Default | Description | -|---|---|---| -| `version` | `latest` | stackql release version (`X.Y.Z`) or `latest` | -| `mode` | `read_only` | MCP server mode: `read_only`, `safe`, `delete_safe`, `full_access` | -| `auth` | (none) | stackql `--auth` JSON for provider credentials | -| `bundle-path` | (none) | install from a local `.mcpb` instead of downloading (testing) | - -## Outputs - -| Output | Description | -|---|---| -| `binary-path` | absolute path to the installed stackql binary | -| `mcp-config` | `mcpServers` JSON for `claude-code-action`'s `mcp_config` input | - -Also exports `STACKQL_MCP_BIN` to the job env (the `@stackql/mcp-server` npm -and `stackql-mcp-server` PyPI wrappers detect it and skip their own download) -and adds the install dir to `PATH`. - -## Example: agentic cloud audit on a schedule - -```yaml -name: nightly-cloud-audit -on: - schedule: - - cron: "0 6 * * *" - -jobs: - audit: - runs-on: ubuntu-latest - permissions: - contents: read - issues: write - steps: - - id: stackql - uses: stackql/setup-stackql-mcp@v1 - with: - mode: read_only - auth: '{"aws":{"type":"aws_signing_v4","credentialsenvvar":"AWS_SECRET_ACCESS_KEY","keyID":"AWS_ACCESS_KEY_ID"}}' - - - uses: anthropics/claude-code-action@v1 - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - mcp_config: ${{ steps.stackql.outputs.mcp-config }} - allowed_tools: "mcp__stackql__*" - prompt: | - Using the stackql tools, audit our AWS account: list S3 buckets - without encryption, security groups open to 0.0.0.0/0, and any - unattached EBS volumes. File a GitHub issue summarising findings - with the SQL you used as evidence. -``` - -## Example: use the binary directly - -```yaml - - id: stackql - uses: stackql/setup-stackql-mcp@v1 - - run: stackql exec "SHOW PROVIDERS" -``` - -## Notes - -- Pin `version` (and this action's tag) for reproducible runs; the registry - entry `io.github.stackql/stackql-mcp` attests the per-version sha256s. -- `read_only` mode means the agent cannot mutate cloud resources regardless - of prompt injection; raise the mode deliberately, never by default. diff --git a/action/action.yml b/action/action.yml deleted file mode 100644 index a7b04d2..0000000 --- a/action/action.yml +++ /dev/null @@ -1,60 +0,0 @@ -# Setup StackQL MCP Server - GitHub Action -# -# Installs the signed stackql binary (sha256-verified against the release -# .sha256) and emits an mcpServers JSON config consumable by MCP-capable -# actions such as anthropics/claude-code-action. Defaults to read_only server -# mode - the safe default for agentic CI. -# -# NOTE: lives in this (internal) repo for development; GitHub Marketplace -# listing requires extraction to a public repo with action.yml at the root -# (planned: stackql/setup-stackql-mcp). - -name: 'Setup StackQL MCP Server' -description: 'Install the signed StackQL binary and emit MCP server config for agentic CI workflows (cloud queries over MCP)' -author: 'StackQL Studios' -branding: - icon: 'database' - color: 'blue' - -inputs: - version: - description: stackql release version (X.Y.Z) or 'latest' - required: false - default: 'latest' - mode: - description: 'MCP server mode: read_only, safe, delete_safe, or full_access' - required: false - default: 'read_only' - auth: - description: stackql --auth JSON for provider credentials (optional) - required: false - default: '' - bundle-path: - description: path to a local .mcpb to install from instead of downloading (CI/testing) - required: false - default: '' - -outputs: - binary-path: - description: absolute path to the installed stackql binary - value: ${{ steps.install.outputs.binary-path }} - mcp-config: - description: mcpServers JSON for MCP-capable actions (claude-code-action mcp_config input) - value: ${{ steps.install.outputs.mcp-config }} - -runs: - using: composite - steps: - - id: install - shell: bash - env: - STACKQL_SETUP_VERSION: ${{ inputs.version }} - STACKQL_SETUP_MODE: ${{ inputs.mode }} - STACKQL_SETUP_AUTH: ${{ inputs.auth }} - STACKQL_SETUP_BUNDLE: ${{ inputs.bundle-path }} - run: | - if command -v python3 >/dev/null 2>&1; then - python3 "${{ github.action_path }}/install.py" - else - python "${{ github.action_path }}/install.py" - fi diff --git a/action/install.py b/action/install.py deleted file mode 100644 index 894047a..0000000 --- a/action/install.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python3 -""" -Installer for the Setup StackQL MCP Server action. Stdlib only. - -Downloads the platform's .mcpb bundle from the stackql GitHub release (or -uses a local bundle via STACKQL_SETUP_BUNDLE), verifies the sha256 against -the published .sha256 asset, extracts the stackql binary, and writes: - - GITHUB_OUTPUT: binary-path, mcp-config (mcpServers JSON, single line) - GITHUB_ENV: STACKQL_MCP_BIN (so the npm/pypi wrappers skip downloading) - GITHUB_PATH: the install directory - -Runs outside Actions too (for local testing): set RUNNER_OS/RUNNER_ARCH or -let it fall back to platform detection; outputs print to stdout when the -GITHUB_* files are absent. -""" -from __future__ import annotations - -import hashlib -import json -import os -import platform -import sys -import urllib.request -import zipfile -from io import BytesIO -from pathlib import Path - -RELEASE_BASE = "https://github.com/stackql/stackql/releases" - - -def log(msg: str) -> None: - print(f"setup-stackql-mcp: {msg}", flush=True) - - -def fail(msg: str) -> None: - print(f"::error::setup-stackql-mcp: {msg}", flush=True) - sys.exit(1) - - -def platform_key() -> str: - os_name = os.environ.get("RUNNER_OS", "").lower() or sys.platform - arch = os.environ.get("RUNNER_ARCH", "").lower() or platform.machine().lower() - is_arm = arch in ("arm64", "aarch64") - if os_name.startswith("linux"): - return "linux-arm64" if is_arm else "linux-x64" - if os_name.startswith(("windows", "win32")): - return "windows-x64" - if os_name.startswith(("macos", "darwin")): - return "darwin-universal" - fail(f"unsupported platform: {os_name}/{arch}") - raise SystemExit # unreachable - - -def fetch(url: str) -> bytes: - req = urllib.request.Request(url, headers={"User-Agent": "setup-stackql-mcp"}) - with urllib.request.urlopen(req) as resp: - return resp.read() - - -def write_kv(env_file_var: str, lines: list[str]) -> None: - path = os.environ.get(env_file_var) - if path: - with open(path, "a", encoding="utf-8") as fh: - fh.write("\n".join(lines) + "\n") - else: - print(f"[{env_file_var}]") - print("\n".join(lines)) - - -def main() -> None: - version = os.environ.get("STACKQL_SETUP_VERSION", "latest").lstrip("v") or "latest" - mode = os.environ.get("STACKQL_SETUP_MODE", "read_only") - auth = os.environ.get("STACKQL_SETUP_AUTH", "") - local_bundle = os.environ.get("STACKQL_SETUP_BUNDLE", "") - - key = platform_key() - bin_name = "stackql.exe" if key == "windows-x64" else "stackql" - bundle_name = f"stackql-mcp-{key}.mcpb" - - if local_bundle: - log(f"installing from local bundle {local_bundle}") - bundle = Path(local_bundle).read_bytes() - else: - base = ( - f"{RELEASE_BASE}/latest/download" - if version == "latest" - else f"{RELEASE_BASE}/download/v{version}" - ) - log(f"downloading {base}/{bundle_name}") - bundle = fetch(f"{base}/{bundle_name}") - expected = fetch(f"{base}/{bundle_name}.sha256").decode().split()[0] - digest = hashlib.sha256(bundle).hexdigest() - if digest != expected: - fail(f"sha256 mismatch for {bundle_name}: expected {expected}, got {digest}") - log(f"sha256 verified: {digest}") - - install_dir = Path( - os.environ.get("RUNNER_TEMP") or Path.home() / ".stackql" / "action" - ) / "stackql-mcp-bin" - install_dir.mkdir(parents=True, exist_ok=True) - bin_path = install_dir / bin_name - with zipfile.ZipFile(BytesIO(bundle)) as zf: - bin_path.write_bytes(zf.read(f"server/{bin_name}")) - bin_path.chmod(0o755) - log(f"installed {bin_path}") - - approot = str(Path.home() / ".stackql") - args = [ - "mcp", - "--mcp.server.type=stdio", - "--approot", approot, - "--mcp.config", json.dumps({"server": {"mode": mode, "audit": {"disabled": True}}}), - ] - if auth: - args += ["--auth", auth] - mcp_config = json.dumps( - {"mcpServers": {"stackql": {"command": str(bin_path), "args": args}}} - ) - - write_kv("GITHUB_OUTPUT", [ - f"binary-path={bin_path}", - f"mcp-config={mcp_config}", - ]) - write_kv("GITHUB_ENV", [f"STACKQL_MCP_BIN={bin_path}"]) - write_kv("GITHUB_PATH", [str(install_dir)]) - - -if __name__ == "__main__": - main() diff --git a/docs/install.md b/docs/install.md index ea8d2e0..450f4e0 100644 --- a/docs/install.md +++ b/docs/install.md @@ -145,6 +145,28 @@ client entry: Add `-e` flags before the image name to pass credential environment variables referenced by your `--auth` config. +## 7. CI / agentic workflows (GitHub Actions) + +For agents running inside CI, the [stackql/setup-stackql-mcp](https://github.com/stackql/setup-stackql-mcp) +action installs the signed binary and emits an MCP server config that plugs into +`anthropics/claude-code-action` and other MCP-capable actions. It defaults to +`read_only` server mode - the safe default for agentic CI. + +```yaml + - id: stackql + uses: stackql/setup-stackql-mcp@v1 + with: + mode: read_only + - uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + mcp_config: ${{ steps.stackql.outputs.mcp-config }} + allowed_tools: "mcp__stackql__*" + prompt: "Using stackql, audit our cloud accounts and summarise findings." +``` + +See the action's README for provider-auth examples and more agentic recipes. + ## Trust model What you get with a fresh StackQL `.mcpb` install: diff --git a/registry/server.template.json b/registry/server.template.json index e02caa5..816038f 100644 --- a/registry/server.template.json +++ b/registry/server.template.json @@ -40,9 +40,7 @@ }, { "registryType": "oci", - "registryBaseUrl": "https://docker.io", - "identifier": "stackql/stackql-mcp", - "version": "__VERSION__", + "identifier": "docker.io/stackql/stackql-mcp:__VERSION__", "transport": { "type": "stdio" } }, {