Spaces:
Running
Running
| #!/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 | |
| "HF_TOKEN", # build_portal creates/uploads the portal Space FROM | |
| # inside this container β without it the portal | |
| # builds fine and then fails on publish. | |
| ] | |
| # 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_<n>_* triple present, plus resolve the KEY_VAR | |
| # indirection: TS_ENV_<n>_KEY_VAR holds the NAME of the var carrying the | |
| # actual trusted-auth key, so that var has to be copied too. | |
| # TS_ENV_<n>_KEY_VAR normally holds the trusted-auth key ITSELF. Only an | |
| # "ENV:<NAME>" 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() | |