Spaces:
Running
Running
File size: 6,886 Bytes
5caaf60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | #!/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()
|