#!/usr/bin/env python """ One-off: copy the MCP Space secrets from the local .env into the TEST MCP Space. source ./demoprep/bin/activate && python scratch/setup_test_mcp_secrets.py Reads values straight from .env and pushes them to `thoughtspot-demoprep/test-mcp` as Repository Secrets. Secret VALUES are never printed — only the key names and whether each was set. HF's API is write-only for secrets, so values cannot be read back out of the prod Space; .env is the source of truth. Why a separate MCP test Space exists: on 2026-08-25 a logging change went straight to the prod MCP Space and failed every build (PermissionError on /app/logs under the Space's non-root user). Deploy to test first. Flags: --dry-run show what would be set, touch nothing --new-token generate a FRESH MCP_ACCESS_TOKEN for test instead of reusing prod's (recommended — a leaked test token should not unlock the prod MCP Space). Prints it ONCE so you can hand it to the calling agent; it is not stored locally. """ import argparse import secrets import sys from pathlib import Path REPO_ID = "thoughtspot-demoprep/test-mcp" ENV_PATH = Path(__file__).resolve().parent.parent / ".env" # What mcp_server.py ACTUALLY reads. Verified 2026-08-26 by grepping the # module, NOT taken from README.md — that table is stale: it lists # MCP_OWNER_EMAIL and MCP_TS_ENV_LABEL, which the server reads nowhere # (MCP_OWNER_EMAIL appears only in tests/e2e_mcp.py; MCP_TS_ENV_LABEL appears # only in the README). The real names are TS_USER_DEFAULT / TS_ENV_URL_DEFAULT. REQUIRED = [ "MCP_ACCESS_TOKEN", # required in http mode — server refuses to boot without it "SUPABASE_URL", # bootstrap: Snowflake + admin settings "SUPABASE_ANON_KEY", "TS_ENV_URL_DEFAULT", # which TS instance a build targets by default "TS_USER_DEFAULT", # owner email when the request omits one ] # At least one LLM key must be present; both are accepted. LLM_KEYS = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"] # Optional but commonly set. OPTIONAL = ["GOOGLE_API_KEY", "MCP_SHARE_WITH", "MCP_MAX_CONCURRENT_BUILDS", "SLACK_BOT_TOKEN", "SLACK_DEPLOYMENT_CHANNEL_ID"] def load_env(path: Path) -> dict: if not path.exists(): sys.exit(f"ERROR: {path} not found — run from the repo root.") env = {} for raw in path.read_text().splitlines(): line = raw.strip() if not line or line.startswith("#") or "=" not in line: continue k, v = line.split("=", 1) env[k.strip()] = v.strip().strip('"').strip("'") return env def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true") ap.add_argument("--new-token", action="store_true") args = ap.parse_args() env = load_env(ENV_PATH) # Collect every TS_ENV__* triple present, plus resolve the KEY_VAR # indirection: TS_ENV__KEY_VAR holds the NAME of the var carrying the # actual trusted-auth key, so that var has to be copied too. # TS_ENV__KEY_VAR normally holds the trusted-auth key ITSELF. Only an # "ENV:" value is an indirection to another variable — this mirrors # _ts_env_table() in mcp_server.py. Never print these values: they ARE the # trusted-auth keys. ts_keys = sorted(k for k in env if k.startswith("TS_ENV_")) indirect = [] for k in ts_keys: if not k.endswith("_KEY_VAR"): continue value = env.get(k, "").strip() if not value.startswith("ENV:"): continue # literal key — copied as-is, nothing to resolve target = value[4:].strip() if target and target in env: indirect.append(target) elif target: print(f" !! {k} dereferences {target}, which is NOT in .env — " f"set {target} on the Space or trusted auth will fail.") llm_present = [k for k in LLM_KEYS if env.get(k)] if not llm_present: print(" !! No LLM key in .env (need ANTHROPIC_API_KEY or OPENAI_API_KEY) " "— builds will fail.") to_set = (REQUIRED + llm_present + [k for k in OPTIONAL if k in env] + ts_keys + indirect) # de-dupe, preserve order seen, ordered = set(), [] for k in to_set: if k not in seen: seen.add(k) ordered.append(k) fresh_token = None if args.new_token: fresh_token = secrets.token_urlsafe(32) missing = [k for k in ordered if not env.get(k) and not (k == "MCP_ACCESS_TOKEN" and fresh_token)] print(f"Target Space: {REPO_ID}") print(f"Reading: {ENV_PATH}") print(f"\n{len(ordered)} secret(s) to set:") for k in ordered: if k == "MCP_ACCESS_TOKEN" and fresh_token: state = "NEWLY GENERATED" elif env.get(k): state = "ok" else: state = "MISSING from .env" print(f" {k:<28} {state}") if missing: print(f"\nWARNING: {len(missing)} required value(s) missing from .env: " f"{', '.join(missing)}") print("The Space will boot but builds will fail until these are set.") if args.dry_run: print("\n--dry-run: nothing was changed.") return if input("\nPush these to the test Space? [y/N] ").strip().lower() != "y": sys.exit("Aborted.") from huggingface_hub import HfApi hf_token = env.get("HF_TOKEN") if not hf_token: sys.exit("ERROR: HF_TOKEN not in .env") api = HfApi(token=hf_token) ok, failed = 0, [] for k in ordered: value = fresh_token if (k == "MCP_ACCESS_TOKEN" and fresh_token) else env.get(k) if not value: continue try: api.add_space_secret(repo_id=REPO_ID, key=k, value=value) print(f" set {k}") ok += 1 except Exception as exc: print(f" FAILED {k}: {type(exc).__name__}: {exc}") failed.append(k) print(f"\nDone: {ok} set, {len(failed)} failed.") if fresh_token: print("\n" + "=" * 62) print("FRESH TEST MCP_ACCESS_TOKEN (shown once — copy it now):") print(f" {fresh_token}") print("Give this to the calling agent for TEST. Prod keeps its own token.") print("=" * 62) print("\nNEXT: confirm the TS target. If MCP_TS_ENV_LABEL / MCP_OWNER_EMAIL") print("match prod, test builds will create objects in the PROD ThoughtSpot") print("instance. Point test at a non-prod TS env or a throwaway owner.") if __name__ == "__main__": main()