diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..7812515 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,250 @@ + + +# build-mcp-server.sh + +Shell script to build a ToolHive-ready MCP server from a scope fixture and OpenAPI spec. It runs validate, generate, dependency sync, and optional quality checks in one pass. + +> **In the node-wire repo:** `scripts/mcp-servers.registry` and `scripts/build-mcp-server.sh` are kept here for convenience, but scope paths (`e2e/fixtures/real/…`) and generated output (`out/-mcp/`) live in the **[mcp-builder](https://github.com/stacklok/mcp-builder)** clone. Pass `--root /path/to/mcp-builder` (or set `MCP_BUILDER_ROOT`) — do not rely on node-wire as the default root. + +--- + +## Prerequisites + +| Requirement | Notes | +|-------------|--------| +| [uv](https://docs.astral.sh/uv/) | On `PATH`, or set `UV` to the full path | +| mcp-builder repo | Clone with `pyproject.toml`, `src/`, and `e2e/fixtures/` | +| [mcp-template-py](https://github.com/stacklok/mcp-template-py) | Default: `../mcp-template-py` relative to mcp-builder root | +| [Task](https://taskfile.dev/) | Optional; skipped with a warning if missing | +| `curl` | Downloads OpenAPI specs when missing | +| `swagger2openapi` | Required for Slack only | + +### One-time setup + +```bash +cd /path/to/mcp-builder +uv sync + +git clone https://github.com/stacklok/mcp-template-py.git ../mcp-template-py +``` + +--- + +## Usage + +### From the repo + +Run from the mcp-builder root when the script lives in `scripts/`: + +```bash +scripts/build-mcp-server.sh spotify +``` + +Builds the Spotify MCP server using the repo as root (resolved automatically from the script location). + +### From anywhere with explicit root + +Pass `--root` when your working directory is not the mcp-builder clone: + +```bash +scripts/build-mcp-server.sh spotify --root G:/SPACE/mcp-builder +``` + +Same clone from WSL: + +```bash +scripts/build-mcp-server.sh spotify --root /mnt/g/SPACE/mcp-builder +``` + +### List servers + +Print all registry aliases and their output directories: + +```bash +scripts/build-mcp-server.sh --list --root G:/SPACE/mcp-builder +``` + +### Options + +Skip quality checks for a faster build: + +```bash +scripts/build-mcp-server.sh github --root G:/SPACE/mcp-builder --skip-check +``` + +--- + +## Repo root resolution + +The script needs the mcp-builder repository root (where `pyproject.toml` lives). First match wins: + +1. `--root PATH` +2. `MCP_BUILDER_ROOT` environment variable +3. Parent of the `scripts/` directory + +When the script lives under **node-wire** (not mcp-builder), option 3 resolves to the node-wire root, which does **not** contain `e2e/fixtures/`. Always use `--root` or `MCP_BUILDER_ROOT` in that case. + +The root must contain `pyproject.toml` and `scripts/mcp-servers.registry`. + +--- + +## Supported servers + +Aliases are defined in `mcp-servers.registry` (pipe-delimited): + +```text +alias|scope_yaml|openapi_spec|download_url|server_name +``` + +| Alias | Output directory | +|-------|------------------| +| `spotify` | `out/spotify-mcp` | +| `github` | `out/github-mcp` | +| `slack` | `out/slack-mcp` | +| `google-drive`, `google_drive` | `out/google-drive-mcp` | +| `stripe` | `out/stripe-mcp` | +| `jira`, `jira-cloud` | `out/jira-cloud-mcp` | +| `bamboohr` | `out/bamboohr-mcp` | +| `twilio` | `out/twilio-mcp` | +| `zoom` | `out/zoom-mcp` | +| `petstore` | `out/petstore-mcp` | + +OpenAPI specs are downloaded on first run when a URL is configured (Slack uses a special Swagger 2.0 conversion — see below). + +--- + +## Options + +| Option | Description | +|--------|-------------| +| `--root PATH` | Path to mcp-builder repo | +| `--list` | List registry aliases and exit | +| `--template DIR` | Path to `mcp-template-py` (default: `../mcp-template-py`) | +| `--skip-download` | Do not fetch OpenAPI spec; fail if file missing | +| `--skip-validate` | Skip `mcp-builder validate` | +| `--skip-sync` | Skip `uv sync` in generated project | +| `--skip-check` | Skip `task check` in generated project | +| `--force` | Remove `out/-mcp` before generate **(default)** | +| `--no-force` | Fail if output directory already exists | +| `-h`, `--help` | Show help | + +### Environment variables + +| Variable | Description | +|----------|-------------| +| `MCP_BUILDER_ROOT` | Default repo root if `--root` is omitted | +| `MCP_TEMPLATE_DIR` | Default template path if `--template` is omitted | +| `UV` | Full path to `uv` when not on PATH | +| `PYTHONUTF8` | Set to `1` on Windows for UTF-8 OpenAPI files (set automatically by the script) | + +--- + +## What the script does + +1. Resolve mcp-builder root and load scope/spec paths from the registry. +2. Download the OpenAPI spec if missing (unless `--skip-download`). +3. Run `uv sync` in the mcp-builder repo. +4. Run `mcp-builder validate` (unless `--skip-validate`). +5. Remove `out/-mcp` if it exists (default `--force`). +6. Run `mcp-builder generate`. +7. Run `uv sync` in the generated project (unless `--skip-sync`). +8. Run `task check` (unless `--skip-check`). + +--- + +## Output + +Generated projects are written to: + +```text +/out/-mcp/ +``` + +Typical layout: + +```text +out/-mcp/ +├── src/_mcp/ +├── deploy/ +├── Dockerfile +├── Taskfile.yml +└── pyproject.toml +``` + +--- + +## Slack OpenAPI download + +Slack’s upstream spec is Swagger 2.0. The registry entry uses `slack:swagger2`, which requires: + +```bash +npm install -g swagger2openapi +``` + +--- + +## Adding a server to the registry + +1. Add a scope YAML under `e2e/fixtures/real/`. +2. Add a line to `mcp-servers.registry`: + +```text +my-api|e2e/fixtures/real/my_api.yaml|e2e/fixtures/real/my_api_openapi.yaml|https://example.com/openapi.yaml|my-api +``` + +3. Build: + +```bash +scripts/build-mcp-server.sh my-api --root /path/to/mcp-builder +``` + +| Field | Meaning | +|-------|---------| +| `alias` | CLI name | +| `scope_yaml` | Path relative to repo root | +| `openapi_spec` | Path relative to repo root | +| `download_url` | `curl` URL, `slack:swagger2`, or empty | +| `server_name` | Output directory: `out/-mcp` | + +--- + +## Platform notes + +### WSL and Windows `uv` + +From WSL, the script can use a Windows `uv.exe` if Linux `uv` is not on PATH. Paths are converted automatically and `PYTHONUTF8` is passed through when needed. + +### Encoding on Windows + +Some OpenAPI specs contain non-ASCII content. The script sets `PYTHONUTF8=1` automatically. If you run `mcp-builder` commands manually on Windows, export `PYTHONUTF8=1` first. + +### Re-running generate + +`mcp-builder generate` fails if the output directory already exists. By default the script removes it (`--force`). Use `--no-force` to keep an existing build and fail instead. + +--- + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| `uv` not found | Install uv or set `UV` to the full path | +| `charmap` codec error | Set `PYTHONUTF8=1` when running mcp-builder manually on Windows | +| `FileExistsError` on generate | Use default `--force`, or delete `out/-mcp` manually | +| `mcp-template-py not found` | Clone the template or pass `--template` | +| Unknown server alias | Run `--list` and check `mcp-servers.registry` | +| Slack download fails | Install `swagger2openapi` | + +--- + +## See also + +- [mcp-builder README](../README.md) +- [e2e/download_openapi_specs.sh](../e2e/download_openapi_specs.sh) +- [mcp-builder on GitHub](https://github.com/stacklok/mcp-builder) — upstream repo for `e2e/fixtures/` and `out/-mcp/deploy/` +- [Google Drive connector (OIDC / ToolHive manifests)](../docs/google_drive_connector.md#user-oauth-oidc--upstream-bearer) +- [Node Wire MCP servers](../docs/mcp-servers.md) diff --git a/scripts/build-mcp-server.sh b/scripts/build-mcp-server.sh new file mode 100644 index 0000000..93e2cac --- /dev/null +++ b/scripts/build-mcp-server.sh @@ -0,0 +1,522 @@ +#!/usr/bin/env bash +## +## SPDX-FileCopyrightText: 2026 AOT Technologies +## SPDX-License-Identifier: Apache-2.0 +## +## Build a ToolHive-ready MCP server from e2e fixture scopes. +## +## Usage: +## scripts/build-mcp-server.sh spotify +## scripts/build-mcp-server.sh spotify --root /path/to/mcp-builder +## scripts/build-mcp-server.sh --list --root G:/SPACE/mcp-builder +## +## Repo root resolution (first match wins): +## 1. --root PATH +## 2. MCP_BUILDER_ROOT environment variable +## 3. Parent of this script's directory (when script lives in repo/scripts/) +## +## Environment: +## MCP_BUILDER_ROOT Path to mcp-builder repo +## MCP_TEMPLATE_DIR Path to mcp-template-py checkout +## UV Full path to uv when not on PATH (common on Windows Git Bash/WSL) +## +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Defaults (overridden by flags) +ROOT_OVERRIDE="" +OUTPUT_DIR="" +TEMPLATE_DIR="${MCP_TEMPLATE_DIR:-}" +DO_DOWNLOAD=1 +DO_VALIDATE=1 +DO_GENERATE=1 +DO_SYNC=1 +DO_CHECK=1 +DO_FORCE=1 +LIST_ONLY=0 +SERVER_ARG="" + +UV_CMD="" + +log() { + echo "==> $*" +} + +info() { + echo " $*" +} + +usage() { + cat <<'EOF' +Usage: scripts/build-mcp-server.sh [options] + scripts/build-mcp-server.sh --list [options] + +Build an MCP server project from e2e fixture scopes (validate → generate → uv sync → task check). + +Arguments: + Registry alias (spotify, github, slack, google-drive, …) + +Options: + --root PATH Path to mcp-builder repo (or set MCP_BUILDER_ROOT) + --list List known server aliases and exit + --template DIR mcp-template-py checkout (default: ../mcp-template-py) + --skip-download Do not fetch OpenAPI spec (fail if missing) + --skip-validate Skip mcp-builder validate + --skip-sync Skip uv sync in generated project + --skip-check Skip task check in generated project + --force Remove out/-mcp before generate (default) + --no-force Fail if output project directory already exists + -h, --help Show this help + +Examples: + scripts/build-mcp-server.sh spotify --root G:/SPACE/mcp-builder + scripts/build-mcp-server.sh github --root /home/user/mcp-builder --skip-check + scripts/build-mcp-server.sh --list --root G:/SPACE/mcp-builder +EOF +} + +normalize_server_arg() { + local s="$1" + s="${s%,}" + s="${s#"${s%%[![:space:]]*}"}" + s="${s%"${s##*[![:space:]]}"}" + echo "$s" +} + +# Convert Windows paths (G:\foo) to a form cd accepts in Git Bash/WSL when possible. +normalize_root_path() { + local p="$1" + if [[ "$p" =~ ^[A-Za-z]:\\ ]] || [[ "$p" =~ ^[A-Za-z]:/ ]]; then + if command -v cygpath &>/dev/null; then + cygpath "$p" + return + fi + if command -v wslpath &>/dev/null; then + wslpath -u "$p" 2>/dev/null && return + fi + # Git Bash: G:\SPACE\mcp-builder → /g/SPACE/mcp-builder + if [[ "$p" =~ ^([A-Za-z]):[/\\](.*)$ ]]; then + local drive="${BASH_REMATCH[1],}" + local rest="${BASH_REMATCH[2]//\\//}" + echo "/${drive}/${rest}" + return + fi + fi + echo "$p" +} + +resolve_mcp_builder_root() { + local root="" + if [[ -n "$ROOT_OVERRIDE" ]]; then + root="$(normalize_root_path "$ROOT_OVERRIDE")" + elif [[ -n "${MCP_BUILDER_ROOT:-}" ]]; then + root="$(normalize_root_path "$MCP_BUILDER_ROOT")" + else + root="$(cd "${SCRIPT_DIR}/.." && pwd)" + fi + + if ! cd "$root" 2>/dev/null; then + echo "ERROR: cannot access mcp-builder root: ${root}" >&2 + exit 1 + fi + ROOT="$(pwd)" + REGISTRY="${ROOT}/scripts/mcp-servers.registry" + OUTPUT_DIR="${ROOT}/out" + + if [[ ! -f "${ROOT}/pyproject.toml" ]]; then + echo "ERROR: not a valid mcp-builder repo (missing pyproject.toml): ${ROOT}" >&2 + exit 1 + fi + if [[ ! -f "$REGISTRY" ]]; then + echo "ERROR: registry not found: ${REGISTRY}" >&2 + echo "Ensure scripts/mcp-servers.registry exists in the repo." >&2 + exit 1 + fi +} + +registry_lines() { + grep -v '^[[:space:]]*#' "$REGISTRY" | grep -v '^[[:space:]]*$' || true +} + +list_servers() { + echo "Known MCP server aliases (scripts/mcp-servers.registry):" + echo " mcp-builder root: ${ROOT}" + echo "" + while IFS='|' read -r alias _scope _spec _url server_name; do + printf " %-16s -> %s/out/%s-mcp\n" "$alias" "$ROOT" "$server_name" + done < <(registry_lines) + echo "" + echo "Run: scripts/build-mcp-server.sh [--root PATH]" +} + +resolve_alias() { + local alias="$1" + local line + line="$(registry_lines | grep -E "^${alias}\\|" | head -n 1 || true)" + if [[ -z "$line" ]]; then + echo "ERROR: unknown server alias: ${alias}" >&2 + echo "Run with --list to see known aliases." >&2 + exit 1 + fi + echo "$line" +} + +uses_windows_uv() { + [[ "$UV_CMD" == *.exe ]] +} + +resolve_uv() { + if [[ -n "$UV_CMD" ]]; then + return 0 + fi + + if [[ -n "${UV:-}" ]]; then + if [[ -x "$UV" || -f "$UV" ]]; then + UV_CMD="$UV" + return 0 + fi + echo "ERROR: UV is set to '${UV}' but that file is not executable." >&2 + exit 1 + fi + + if command -v uv &>/dev/null; then + UV_CMD="$(command -v uv)" + return 0 + fi + if command -v uv.exe &>/dev/null; then + UV_CMD="$(command -v uv.exe)" + return 0 + fi + + local candidates=( + "${ROOT}/.venv/Scripts/uv.exe" + "${ROOT}/.venv/bin/uv" + "${HOME}/.local/bin/uv" + "${HOME}/.local/bin/uv.exe" + ) + + if [[ -n "${USERPROFILE:-}" ]]; then + local uf="${USERPROFILE//\\//}" + candidates+=( + "${uf}/.local/bin/uv.exe" + "${uf}/AppData/Roaming/Python/Python313/Scripts/uv.exe" + ) + fi + + if [[ -d "/mnt/c/Users" ]] && command -v cmd.exe &>/dev/null; then + local wuser + wuser="$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r\n' | xargs)" || true + if [[ -n "$wuser" ]]; then + candidates+=( + "/mnt/c/Users/${wuser}/.local/bin/uv.exe" + "/mnt/c/Users/${wuser}/AppData/Roaming/Python/Python313/Scripts/uv.exe" + ) + fi + fi + + local c + for c in "${candidates[@]}"; do + if [[ -f "$c" ]]; then + UV_CMD="$c" + return 0 + fi + done + + if command -v cmd.exe &>/dev/null; then + local winpath + winpath="$(cmd.exe /c "where uv" 2>/dev/null | head -n 1 | tr -d '\r\n' | xargs)" || true + if [[ -n "$winpath" ]]; then + if command -v wslpath &>/dev/null; then + UV_CMD="$(wslpath "$winpath" 2>/dev/null || true)" + elif command -v cygpath &>/dev/null; then + UV_CMD="$(cygpath "$winpath" 2>/dev/null || true)" + elif [[ "$winpath" =~ ^([A-Za-z]):\\(.*)$ ]]; then + local drive="${BASH_REMATCH[1],}" + local rest="${BASH_REMATCH[2]//\\//}" + UV_CMD="/${drive}/${rest}" + else + UV_CMD="$winpath" + fi + if [[ -n "$UV_CMD" && -f "$UV_CMD" ]]; then + return 0 + fi + fi + fi + + echo "ERROR: 'uv' not found." >&2 + echo " Install: https://docs.astral.sh/uv/" >&2 + echo " Or set UV to the full path to uv." >&2 + exit 1 +} + +require_uv() { + resolve_uv + info "using uv: ${UV_CMD}" +} + +setup_python_utf8() { + export PYTHONUTF8=1 + # WSL: Windows uv.exe does not inherit Linux env unless listed in WSLENV. + if uses_windows_uv && [[ -f /proc/version ]] && grep -qi microsoft /proc/version; then + case ":${WSLENV:-}:" in + *:PYTHONUTF8:*) ;; + *) export WSLENV="${WSLENV:+${WSLENV}:}PYTHONUTF8" ;; + esac + fi +} + +to_uv_path() { + local p="$1" + if uses_windows_uv && command -v wslpath &>/dev/null; then + wslpath -w "$p" + elif uses_windows_uv && command -v cygpath &>/dev/null; then + cygpath -w "$p" + else + echo "$p" + fi +} + +run() { + log "$*" + "$@" +} + +remove_existing_project() { + local dir="$1" + if [[ ! -d "$dir" ]]; then + return 0 + fi + log "Removing existing project: ${dir}" + chmod -R u+w "$dir" 2>/dev/null || true + rm -rf "$dir" +} + +download_slack_spec() { + local spec_path="$1" + local tmp + tmp="$(mktemp "${TMPDIR:-/tmp}/slack_swagger.XXXXXX.json")" + curl -fsSL -o "$tmp" \ + "https://raw.githubusercontent.com/slackapi/slack-api-specs/master/web-api/slack_web_openapi_v2.json" + if ! command -v swagger2openapi &>/dev/null; then + rm -f "$tmp" + echo "ERROR: slack requires swagger2openapi. Install: npm install -g swagger2openapi" >&2 + exit 1 + fi + swagger2openapi "$tmp" -o "$spec_path" --yaml + rm -f "$tmp" +} + +download_spec() { + local spec_rel="$1" + local download_url="$2" + local spec_path="${ROOT}/${spec_rel}" + + if [[ -z "$download_url" ]]; then + if [[ ! -f "$spec_path" ]]; then + echo "ERROR: OpenAPI spec not found: ${spec_path} (no download URL configured)" >&2 + exit 1 + fi + info "OpenAPI spec present: ${spec_rel}" + return 0 + fi + + if [[ -f "$spec_path" ]]; then + info "OpenAPI spec found (skipping download): ${spec_path}" + return 0 + fi + + mkdir -p "$(dirname "$spec_path")" + if [[ "$download_url" == "slack:swagger2" ]]; then + log "Downloading and converting Slack spec → ${spec_rel}" + download_slack_spec "$spec_path" + return 0 + fi + + log "Downloading OpenAPI spec → ${spec_rel}" + curl -fsSL -o "$spec_path" "$download_url" +} + +build_one() { + local alias="$1" + local scope_rel spec_rel download_url server_name + local scope_path spec_path project_dir template_dir + + UV_CMD="" + + IFS='|' read -r _alias scope_rel spec_rel download_url server_name <<< "$(resolve_alias "$alias")" + scope_path="${ROOT}/${scope_rel}" + spec_path="${ROOT}/${spec_rel}" + project_dir="${OUTPUT_DIR}/${server_name}-mcp" + + if [[ -n "$TEMPLATE_DIR" ]]; then + template_dir="$(normalize_root_path "$TEMPLATE_DIR")" + else + template_dir="$(cd "${ROOT}/../mcp-template-py" 2>/dev/null && pwd || echo "${ROOT}/../mcp-template-py")" + fi + + if [[ ! -d "$template_dir" ]]; then + echo "ERROR: mcp-template-py not found at: ${template_dir}" >&2 + echo "Clone: git clone https://github.com/stacklok/mcp-template-py.git " >&2 + echo "Or pass: --template PATH" >&2 + exit 1 + fi + + if [[ ! -f "$scope_path" ]]; then + echo "ERROR: scope not found: ${scope_path}" >&2 + exit 1 + fi + + if [[ "$DO_DOWNLOAD" -eq 1 ]]; then + download_spec "$spec_rel" "$download_url" + elif [[ ! -f "$spec_path" ]]; then + echo "ERROR: OpenAPI spec not found: ${spec_path}" >&2 + exit 1 + fi + + echo "" + log "Building MCP server: ${server_name} (alias: ${alias})" + echo " mcp-builder root: ${ROOT}" + echo " scope: ${scope_rel}" + echo " spec: ${spec_rel}" + echo " output: ${project_dir}" + + cd "$ROOT" + require_uv + setup_python_utf8 + echo " PYTHONUTF8=1" + + run "$UV_CMD" sync + + if [[ "$DO_VALIDATE" -eq 1 ]]; then + run "$UV_CMD" run mcp-builder validate "$(to_uv_path "$scope_path")" --openapi-spec "$(to_uv_path "$spec_path")" + fi + + if [[ "$DO_GENERATE" -eq 1 ]]; then + if [[ "$DO_FORCE" -eq 1 ]]; then + remove_existing_project "$project_dir" + fi + run "$UV_CMD" run mcp-builder generate \ + "$(to_uv_path "$scope_path")" \ + "$(to_uv_path "$spec_path")" \ + "$(to_uv_path "$template_dir")" \ + --output-dir "$(to_uv_path "$OUTPUT_DIR")" + fi + + if [[ "$DO_SYNC" -eq 1 ]]; then + if [[ ! -d "$project_dir" ]]; then + echo "ERROR: expected project at ${project_dir}" >&2 + exit 1 + fi + log "Installing dependencies in ${project_dir}" + (cd "$project_dir" && "$UV_CMD" sync) + fi + + if [[ "$DO_CHECK" -eq 1 ]]; then + if ! command -v task &>/dev/null; then + echo "WARNING: 'task' not found; skipping task check. Install: https://taskfile.dev/" >&2 + else + log "Running task check in ${project_dir}" + (cd "$project_dir" && task check) + fi + fi + + cat <&2 + exit 2 + fi + ROOT_OVERRIDE="$2" + shift 2 + ;; + --template) + TEMPLATE_DIR="$2" + shift 2 + ;; + --skip-download) + DO_DOWNLOAD=0 + shift + ;; + --skip-validate) + DO_VALIDATE=0 + shift + ;; + --skip-sync) + DO_SYNC=0 + shift + ;; + --skip-check) + DO_CHECK=0 + shift + ;; + --force) + DO_FORCE=1 + shift + ;; + --no-force) + DO_FORCE=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + break + ;; + -*) + echo "Unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + *) + if [[ -z "$SERVER_ARG" ]]; then + arg="$(normalize_server_arg "$1")" + if [[ -n "$arg" ]]; then + SERVER_ARG="$arg" + fi + else + extra="$(normalize_server_arg "$1")" + if [[ -n "$extra" ]]; then + echo "Unexpected argument: $1" >&2 + usage >&2 + exit 2 + fi + fi + shift + ;; + esac +done + +resolve_mcp_builder_root + +if [[ "$LIST_ONLY" -eq 1 ]]; then + list_servers + exit 0 +fi + +if [[ -z "$SERVER_ARG" ]]; then + usage >&2 + exit 2 +fi + +build_one "$SERVER_ARG" diff --git a/scripts/mcp-servers.registry b/scripts/mcp-servers.registry new file mode 100644 index 0000000..a7024ae --- /dev/null +++ b/scripts/mcp-servers.registry @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2026 AOT Technologies +# SPDX-License-Identifier: Apache-2.0 +# +# MCP server registry for scripts/build-mcp-server.sh +# +# Format (pipe-delimited, one alias per line): +# alias|scope_yaml|openapi_spec|download_url|server_name +# +# download_url: +# - https://... curl the spec into openapi_spec path +# - slack:swagger2 download Swagger 2.0 and convert with swagger2openapi +# - (empty) spec must already exist on disk (no download) +# +# server_name sets the generated project directory: out/-mcp/ + +google-drive|e2e/fixtures/real/google_drive.yaml|e2e/fixtures/real/google_drive_openapi.yaml|https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/googleapis.com/drive/v3/openapi.yaml|google-drive +google_drive|e2e/fixtures/real/google_drive.yaml|e2e/fixtures/real/google_drive_openapi.yaml|https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/googleapis.com/drive/v3/openapi.yaml|google-drive + +github|e2e/fixtures/real/github.yaml|e2e/fixtures/real/github_openapi.yaml|https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml|github + +bamboohr|e2e/fixtures/real/bamboohr.yaml|e2e/fixtures/real/bamboohr_openapi.yaml|https://openapi.bamboohr.io/main/latest/docs/openapi/public-openapi.yaml|bamboohr + +jira|e2e/fixtures/real/jira.yaml|e2e/fixtures/real/jira_openapi.yaml|https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json|jira-cloud +jira-cloud|e2e/fixtures/real/jira.yaml|e2e/fixtures/real/jira_openapi.yaml|https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json|jira-cloud + +slack|e2e/fixtures/real/slack.yaml|e2e/fixtures/real/slack_openapi.yaml|slack:swagger2|slack + +petstore|e2e/fixtures/real/petstore.yaml|e2e/fixtures/real/petstore_openapi.json|https://petstore3.swagger.io/api/v3/openapi.json|petstore + +stripe|e2e/fixtures/real/stripe.yaml|e2e/fixtures/real/stripe_openapi.yaml|https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.yaml|stripe + +twilio|e2e/fixtures/real/twilio.yaml|e2e/fixtures/real/twilio_openapi.yaml|https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/yaml/twilio_api_v2010.yaml|twilio + +spotify|e2e/fixtures/real/spotify.yaml|e2e/fixtures/real/spotify_openapi.yaml|https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/spotify.com/1.0.0/openapi.yaml|spotify + +zoom|e2e/fixtures/real/zoom.yaml|e2e/fixtures/real/zoom_openapi.yaml|https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/zoom.us/2.0.0/openapi.yaml|zoom