test-mcp / scripts /setup_test_mcp_secrets.py
mikeboone's picture
docs: single state-of-play snapshot + HF_TOKEN in the test-secrets script
5caaf60
Raw
History Blame Contribute Delete
6.89 kB
#!/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()