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()