File size: 25,998 Bytes
a3c5476
 
 
 
170fbda
 
 
2b98b10
 
 
 
 
 
 
 
 
 
 
170fbda
 
 
aedcb5d
 
 
 
 
a3c5476
170fbda
 
2b98b10
170fbda
 
a3c5476
 
 
 
 
aedcb5d
a3c5476
170fbda
 
a3c5476
2b98b10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3c5476
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
879624b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3c5476
 
 
 
 
 
 
 
 
879624b
 
 
a3c5476
 
879624b
a3c5476
 
 
 
 
879624b
 
 
 
 
 
 
 
 
 
 
 
a3c5476
 
 
 
879624b
a3c5476
 
 
 
 
 
2b98b10
a3c5476
 
 
 
 
 
 
 
 
 
 
 
 
879624b
a3c5476
 
879624b
a3c5476
 
879624b
 
 
 
 
 
 
a3c5476
 
 
 
 
879624b
a3c5476
879624b
 
 
 
 
a3c5476
879624b
 
 
 
2b98b10
a3c5476
 
 
170fbda
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
879624b
 
 
2b98b10
170fbda
 
 
 
 
 
 
 
 
879624b
 
170fbda
879624b
 
 
 
 
170fbda
 
879624b
170fbda
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b98b10
170fbda
 
 
 
 
 
 
aedcb5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3c5476
2b98b10
 
 
 
 
 
 
 
aedcb5d
 
 
2b98b10
aedcb5d
 
 
 
 
2b98b10
aedcb5d
 
a3c5476
 
2b98b10
 
 
 
aedcb5d
 
2b98b10
aedcb5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4933ad8
 
 
 
 
 
 
 
 
 
 
 
aedcb5d
 
 
2b98b10
aedcb5d
 
 
 
 
 
2b98b10
aedcb5d
 
 
 
4933ad8
 
 
 
 
 
aedcb5d
 
2b98b10
aedcb5d
 
4933ad8
 
 
aedcb5d
 
 
 
 
 
 
 
a3c5476
aedcb5d
 
a3c5476
 
aedcb5d
a3c5476
aedcb5d
 
 
a3c5476
aedcb5d
 
 
 
 
 
 
 
 
 
170fbda
 
aedcb5d
 
 
 
 
 
 
 
 
 
 
2b98b10
170fbda
aedcb5d
 
 
 
 
 
 
 
170fbda
 
aedcb5d
170fbda
aedcb5d
 
 
170fbda
 
aedcb5d
 
2b98b10
aedcb5d
170fbda
 
aedcb5d
 
 
 
 
4933ad8
 
 
a3c5476
 
170fbda
 
aedcb5d
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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
"""DC Hub — Power Index demo + MCP server (Hugging Face Space).

A live demo of the DC Hub Power Index (DCPI) that ALSO runs as an MCP server
(launch(mcp_server=True)) so Hugging Face Agents / smolagents and any MCP client
can query it. Seven tools: the two DCPI classics (dcpi_score, compare_markets)
plus five bridge tools (grid scoreboard, facility search, market ranking,
interconnection queue, hyperscaler deals) forwarded to DC Hub's canonical MCP
server. The complete DC Hub MCP — every facility, market and deal in the
index, plus real-time grid telemetry, fiber and gas — lives at
https://dchub.cloud/mcp. Connect that for the full dataset (10 calls/day free
anonymous; free key via its claim_free_key tool).

★ COUNTS ARE NOT TYPED IN THIS FILE. They resolve from DC Hub's canon endpoint
  (/api/v1/canon/phrases) via _canon() below, because a typed count here rots
  silently: this Space shipped a facilities figure ABOVE canon's citation-safe
  floor — an OVER-claim, the one direction a floored count must never take. Docstrings are the exception and state no count at all: they are
  the MCP tool schema, they are read once at import, and a network call in the
  import path is its own defect.

Optional Space secret DCHUB_API_KEY (a dchub_ key) lifts the bridge tools to
full-tier responses; without it they ride the anonymous free tier.

UI NOTE: the 7 tool functions below are the MCP contract (names + docstrings
become the tool schema at /gradio_api/mcp/schema) — keep them stable. UI-only
events that reuse them (e.g. textbox .submit) pass api_name=False so they
never mint a duplicate MCP tool.
"""
import json
import os
import time
import uuid

import requests
import gradio as gr

API = "https://dchub.cloud"
FULL_MCP = "https://dchub.cloud/mcp"
SPACE_MCP = "https://dchubcloud-dchub.hf.space/gradio_api/mcp/sse"
TIMEOUT = 20
API_KEY = (os.environ.get("DCHUB_API_KEY") or "").strip()
UA = "huggingface-space-dchub-bridge/1.0 (+https://dchub.cloud)"

# ── Canon-derived public counts ────────────────────────────────────────────
# DC Hub publishes its own citation-safe floors at /api/v1/canon/phrases. They
# are floored DOWN from the live measurement and are never above reality, which
# is exactly the property a public, CC-BY-4.0 surface needs.
#
# Fail-open by construction: an unreadable canon yields "" for every phrase and
# the copy renders COUNT-FREE. That asymmetry is deliberate — a missing number
# is visible and self-correcting, a stale one is neither.
_CANON_TTL = 3600
_CANON_TIMEOUT = 6          # short: this runs once at Space boot, before the UI
_canon_cache = {"at": 0.0, "val": {}}


def _canon():
    """Live public floors: {'facilities','deals','markets','tools'} or {}."""
    now = time.time()
    if _canon_cache["val"] and (now - _canon_cache["at"]) < _CANON_TTL:
        return _canon_cache["val"]
    try:
        r = requests.get(f"{API}/api/v1/canon/phrases",
                         timeout=_CANON_TIMEOUT, headers={"User-Agent": UA})
        d = r.json() if r.ok else {}
        val = {k: str(d[k]) for k in ("facilities", "deals", "markets", "tools")
               if d.get(k)}
    except Exception:
        val = {}
    if val:
        _canon_cache.update(at=now, val=val)
    return _canon_cache["val"]


def _phrase(key, noun):
    """'20,400+ facilities', or '' when canon cannot be read.

    Callers must tolerate the empty string — that is the whole point. Build
    sentences so an absent phrase leaves them grammatical, never mangled.
    """
    v = _canon().get(key)
    return f"{v} {noun}" if v else ""


def _tools_clause():
    """' + 83 tools', or '' — a clause that may vanish without breaking prose."""
    p = _phrase("tools", "tools")
    return f" + {p}" if p else ""


def _tools_strong():
    """'<strong>83 tools</strong>, ' or '' — trailing comma travels with it."""
    p = _phrase("tools", "tools")
    return f"<strong>{p}</strong>, " if p else ""


def _verdict(score):
    try:
        s = float(score)
    except (TypeError, ValueError):
        return "—"
    if s >= 60:
        return "🟢 BUILD"
    if s >= 30:
        return "🟡 CAUTION"
    return "🔴 AVOID"


def _slug(market):
    return (market or "").strip().lower().replace(",", "").replace(" ", "-")


# ── DCPI reads ─────────────────────────────────────────────────────────────
# /api/v1/dcpi/scores/<slug> answers a keyless caller with a PREVIEW: the
# BUILD / CAUTION / AVOID verdict and the market identity are free, but every
# number (composite_score, avg_kwh_cents, queue_wait_months, ...) comes back
# null with "_gated": true. Printing those nulls read as "None/100" and a "—"
# verdict, and compare_markets coerced them to 0.0 and ranked every market
# AVOID — Cheyenne (BUILD) included. So: use the API's own verdict when the
# score is locked, say plainly that the number needs a key, and quote the
# API's own _upgrade_cta rather than typing a price here.
_VERDICT_BADGE = {"BUILD": "🟢 BUILD", "CAUTION": "🟡 CAUTION", "AVOID": "🔴 AVOID"}
_VERDICT_ORDER = {"BUILD": 0, "CAUTION": 1, "AVOID": 2}


def _dcpi_get(market):
    headers = {"User-Agent": UA}
    if API_KEY:
        headers["X-API-Key"] = API_KEY
    return requests.get(f"{API}/api/v1/dcpi/scores/{_slug(market)}",
                        timeout=TIMEOUT, headers=headers)


def _num(v):
    return v if isinstance(v, (int, float)) and not isinstance(v, bool) else None


def _dcpi_verdict_word(d):
    """'BUILD' / 'CAUTION' / 'AVOID' from the score, else the API's verdict, else ''."""
    score = _num(d.get("composite_score"))
    if score is not None:
        return _verdict(score).split()[-1]
    v = str(d.get("verdict") or "").strip().upper()
    return v if v in _VERDICT_BADGE else ""


def _score_locked(d):
    return _num(d.get("composite_score")) is None


def _key_note(d):
    cta = (d.get("_upgrade_cta") or "").strip()
    return ("The numeric score, power cost and time-to-power need a DC Hub API key"
            + (f". {cta}" if cta else ".")
            + f" Or connect the DC Hub MCP → **{FULL_MCP}**.")


def dcpi_score(market: str) -> str:
    """Get the DC Hub Power Index (DCPI) for a U.S. data-center market: a 0-100
    power-availability score with a BUILD / CAUTION / AVOID verdict, average power
    cost (cents/kWh), and modeled time-to-power (months). Answers "can I actually
    get power to build a data center in this market, and how soon?".

    Args:
        market: A U.S. market name, e.g. "Northern Virginia", "Phoenix", "Columbus",
                "Dallas", "Atlanta", "Cheyenne", "Omaha", "Tulsa".

    Without a DC Hub API key the verdict is returned and the numeric score is
    reported as needing a key (it is never printed as a number it isn't).
    """
    try:
        r = _dcpi_get(market)
        if r.status_code == 404:
            return (f"No DCPI data for **{market}**. Try a major U.S. market — Northern "
                    f"Virginia, Phoenix, Columbus, Dallas, Atlanta, Cheyenne, Omaha, Tulsa.")
        r.raise_for_status()
        d = r.json()
        name = d.get("market_name", market.title())
        where = ", ".join(x for x in (d.get("state"), d.get("iso")) if x)
        head = f"### {name}" + (f" ({where})" if where else "") + " — DC Hub Power Index\n"
        if _score_locked(d):
            word = _dcpi_verdict_word(d)
            verdict_line = (f"- **Verdict: {_VERDICT_BADGE[word]}**\n" if word
                            else "- Verdict: not in the keyless response\n")
            return (
                head + verdict_line +
                f"- **Score: needs a key.** {_key_note(d)}\n\n"
                f"_DC Hub (dchub.cloud) · CC-BY-4.0._"
            )
        score = d.get("composite_score")
        ttp = d.get("queue_wait_months")
        ttp_s = f"~{int(ttp)} months" if isinstance(ttp, (int, float)) else "n/a"
        return (
            head +
            f"- **Verdict: {_verdict(score)}**  ·  composite **{score}/100**\n"
            f"- Modeled time-to-power: **{ttp_s}**\n"
            f"- Avg power cost: **{d.get('avg_kwh_cents', '?')} ¢/kWh**\n"
            f"- Excess-power headroom: {d.get('excess_power_score', '?')}/100  ·  "
            f"grid constraint: {d.get('constraint_score', '?')}/100\n\n"
            f"_Modeled estimate from public ISO/EIA/queue data · DC Hub (dchub.cloud) · CC-BY-4.0._\n\n"
            f"_Full grid headroom, interconnection queue, fiber{_tools_clause()}: connect the DC Hub MCP → **{FULL_MCP}**._"
        )
    except Exception as e:
        return f"DC Hub lookup failed ({e}). Query the full live MCP at {FULL_MCP}."


def compare_markets(markets: str) -> str:
    """Rank several U.S. data-center markets by the DC Hub Power Index (DCPI),
    best power-availability (BUILD) first, to decide where to build.

    Args:
        markets: Comma-separated market names, e.g.
                 "Northern Virginia, Phoenix, Cheyenne, Omaha".
    """
    rows, locked = [], None
    for m in [x.strip() for x in (markets or "").split(",") if x.strip()][:8]:
        try:
            r = _dcpi_get(m)
            if r.status_code == 200:
                d = r.json()
                word = _dcpi_verdict_word(d)
                score = _num(d.get("composite_score"))
                if score is None and locked is None:
                    locked = d
                # Verdict first, then score (locked scores sort last within a verdict).
                rows.append((_VERDICT_ORDER.get(word, 3), -(score if score is not None else -1),
                             d.get("market_name", m.title()), word, score,
                             d.get("avg_kwh_cents"), d.get("queue_wait_months")))
        except Exception:
            pass
    if not rows:
        return "No DCPI data for those markets. Try major U.S. metros."
    rows.sort()
    out = ["### DCPI ranking — best power-availability first\n"]
    for _o, _s, name, word, score, cost, ttp in rows:
        badge = _VERDICT_BADGE.get(word, "verdict n/a")
        if score is None:
            out.append(f"- {badge} **{name}** — score needs a key")
            continue
        ttp_s = f"~{int(ttp)}mo" if isinstance(ttp, (int, float)) else "n/a"
        cost_s = f"{cost}¢/kWh" if _num(cost) is not None else "cost n/a"
        out.append(f"- {badge} **{name}** — {score}/100 · {cost_s} · {ttp_s} to power")
    if locked is not None:
        out.append("\n_Ranked by verdict: " + _key_note(locked).replace("**", "") + "_")
    out.append(f"\n_Full ranking across {_phrase('markets', 'markets') or 'every scored market'}: DC Hub MCP → {FULL_MCP}._")
    return "\n".join(out)


# ── Bridge tools: forwarded to the canonical DC Hub MCP server ──────────────
def _call_tool(name: str, arguments: dict) -> str:
    """Single-shot JSON-RPC tools/call against the canonical DC Hub MCP server."""
    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream",
        "User-Agent": UA,
        "X-MCP-Platform": "huggingface",
    }
    if API_KEY:
        headers["X-API-Key"] = API_KEY
    body = {
        "jsonrpc": "2.0",
        "id": str(uuid.uuid4()),
        "method": "tools/call",
        "params": {"name": name, "arguments": {k: v for k, v in arguments.items()
                                               if v not in ("", None)}},
    }
    try:
        r = requests.post(FULL_MCP, json=body, headers=headers, timeout=45)
        text = r.text
        if "data:" in text and text.lstrip().startswith("event:"):
            for line in text.splitlines():
                if line.startswith("data:"):
                    text = line[5:].strip()
                    break
        payload = json.loads(text)
        result = payload.get("result") or payload
        content = result.get("content")
        if isinstance(content, list):
            out = "\n".join(c.get("text", "") for c in content if c.get("type") == "text")
        else:
            out = json.dumps(result, indent=2)
        try:
            return json.dumps(json.loads(out), indent=2)
        except Exception:
            return out
    except Exception as e:
        return json.dumps({"error": str(e)[:300],
                           "hint": f"DC Hub MCP unreachable — retry, or connect {FULL_MCP} directly"})


def grid_scoreboard() -> str:
    """Live ranked scoreboard of power grids (7 US ISOs + Great Britain, EU zones,
    Taiwan, Japan, South Korea, Brazil, and more): fuel mix, renewable share, and
    demand right now. Greenest grids first. No parameters."""
    return _call_tool("get_grid_scoreboard", {})


def search_facilities(query: str = "", country: str = "", state: str | None = None,
                      operator: str | None = None, min_capacity_mw: float | None = None,
                      limit: int | None = 10) -> str:
    """Search DC Hub's global index of data-center facilities (many independently
    verified) across 170+ countries.

    Args:
        query: Free-text search over facility name/operator/location.
        country: ISO 3166-1 alpha-2 country code, e.g. US, GB, SG.
        state: US state abbreviation, e.g. VA, TX.
        operator: Operator/provider name, e.g. Equinix, Digital Realty.
        min_capacity_mw: Minimum power capacity in megawatts.
        limit: Max results (1-500).

    Every argument is optional; pass only the filters you need.
    """
    # Gradio marks an MCP/API argument required unless its UI component has an
    # initial value or the Python default is None. State, Operator and Min
    # capacity had neither, so a call with just {"query": ...} failed with
    # "No value provided for required argument: state". The None defaults above
    # are what make them optional in /gradio_api/mcp/schema.
    return _call_tool("search_facilities", {
        "query": query, "country": country, "state": state, "operator": operator,
        "min_capacity_mw": min_capacity_mw or None, "limit": int(limit or 0) or 10})


def rank_markets(criteria: str = "most_capacity", region: str = "us",
                 limit: int = 10) -> str:
    """Rank data-center markets by a live criterion from the DC Hub Power Index.

    Args:
        criteria: One of cheapest_power, most_capacity, most_operators, fastest.
        region: global, us, canada, eu, apac, or americas.
        limit: Number of markets to return (1-50).
    """
    return _call_tool("rank_markets", {"criteria": criteria, "region": region,
                                       "limit": int(limit) or 10})


def interconnection_queue(iso: str = "") -> str:
    """US grid interconnection-queue intelligence — how much capacity is waiting to
    connect, by ISO/RTO.

    Args:
        iso: Optional drill-down: ERCOT, PJM, MISO, CAISO, SPP, NYISO, or ISONE.
             Empty = national overview.
    """
    return _call_tool("get_interconnection_queue", {"iso": iso})


def hyperscaler_deals(limit: int = 10) -> str:
    """Recent hyperscaler / AI-capex deals from DC Hub's deal tracker.

    Args:
        limit: Number of recent deals to return.
    """
    return _call_tool("hyperscaler_deals", {"limit": int(limit) or 10})


# ── UI ──────────────────────────────────────────────────────────────────────
# Everything below is presentation only — no new MCP tools are minted here
# (UI-only events pass api_name=False).

CSS = """
.gradio-container { max-width: 1080px !important; margin: 0 auto !important; }
.dc-hero {
  border-radius: 14px;
  padding: 26px 28px 22px;
  margin-bottom: 4px;
  background: linear-gradient(120deg, #0b1220 0%, #101c33 55%, #1a2a17 100%);
  border: 1px solid rgba(250, 204, 21, .25);
}
.dc-hero h1 {
  margin: 0 0 6px;
  font-size: 1.65rem;
  line-height: 1.2;
  color: #f8fafc;
}
.dc-hero h1 .amp { color: #facc15; }
.dc-hero .sub {
  margin: 0 0 14px;
  font-size: 1rem;
  color: #cbd5e1;
  max-width: 62rem;
}
.dc-hero .sub a { color: #facc15; text-decoration: none; }
.dc-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.dc-chip {
  font-size: .8rem;
  padding: 4px 11px;
  border-radius: 999px;
  background: rgba(148, 163, 184, .14);
  border: 1px solid rgba(148, 163, 184, .28);
  color: #e2e8f0;
  white-space: nowrap;
}
.dc-chip.build   { background: rgba(34, 197, 94, .14);  border-color: rgba(34, 197, 94, .45);  color: #86efac; }
.dc-chip.caution { background: rgba(234, 179, 8, .14);  border-color: rgba(234, 179, 8, .45);  color: #fde047; }
.dc-chip.avoid   { background: rgba(239, 68, 68, .14);  border-color: rgba(239, 68, 68, .45);  color: #fca5a5; }
.dc-foot { text-align: center; font-size: .85rem; opacity: .8; margin-top: 6px; }
.dc-mcp-note { font-size: .9rem; margin: 2px 0 8px; }
"""

def _hero():
    """Built when the UI is constructed, so canon is read once per boot."""
    _mk = _phrase("markets", "markets")
    _fa = _phrase("facilities", "facilities")
    _dl = _phrase("deals", "tracked deals")
    _chips = "".join(
        f'<span class="dc-chip">{c}</span>' for c in (_mk, _fa, "live grid telemetry", _dl, "CC-BY-4.0") if c)
    return f"""
<div class="dc-hero">
  <h1><span class="amp">⚡</span> DC Hub — Power Index</h1>
  <p class="sub"><strong>Can you actually get power to build a data center here?</strong>
  Live 0–100 power-availability scores for data-center markets worldwide, with a verdict,
  power cost, and modeled time-to-power — from <a href="https://dchub.cloud">dchub.cloud</a>.</p>
  <div class="dc-chips">
    <span class="dc-chip build">🟢 BUILD ≥ 60</span>
    <span class="dc-chip caution">🟡 CAUTION 30–59</span>
    <span class="dc-chip avoid">🔴 AVOID &lt; 30</span>
    {_chips}
  </div>
</div>
"""

def _claude_code_snippet():
    _t = _phrase("tools", "tools")
    _full = f"({_t}; 10 calls/day free, no key needed)" if _t else "(10 calls/day free, no key needed)"
    return f"""# This Space (7 tools, free)
claude mcp add --transport sse dchub-power-index {SPACE_MCP}

# Full DC Hub MCP {_full}
claude mcp add --transport http dchub {FULL_MCP}"""

JSON_SNIPPET = f"""{{
  "mcpServers": {{
    "dchub-power-index": {{ "url": "{SPACE_MCP}" }},
    "dchub":             {{ "url": "{FULL_MCP}" }}
  }}
}}"""

SMOLAGENTS_SNIPPET = f"""from smolagents import CodeAgent, InferenceClientModel
from smolagents.mcp_client import MCPClient

with MCPClient({{"url": "{SPACE_MCP}"}}) as tools:
    agent = CodeAgent(tools=tools, model=InferenceClientModel())
    agent.run("Which U.S. market should I build a 100MW data center in?")"""


# DeepSeek Harness (deepseek-ai/deepseek-harness) — its MCP client plugin is
# @deepseek-ai/dsh-mcp-client; transport streamable-http takes url + headers.
# ★ Points at FULL_MCP, not this Space: the harness can hold a key, so there is
#   no reason to route it through a 7-tool bridge. DeepSeek's CONSUMER chat has
#   no MCP UI at all — that is why this tab names the harness, not "DeepSeek".
DEEPSEEK_SNIPPET = f"""- id: mcp-dchub
  name: '@deepseek-ai/dsh-mcp-client'
  config:
    serverName: dchub
    transport: streamable-http
    url: {FULL_MCP}"""

with gr.Blocks(title="DC Hub — Power Index",
               theme=gr.themes.Soft(primary_hue="amber", neutral_hue="slate"),
               css=CSS) as demo:
    gr.HTML(_hero())

    with gr.Accordion("🔌 Connect via MCP — Claude Code · Claude Desktop · Cursor · smolagents",
                      open=False):
        gr.Markdown(
            f"<p class='dc-mcp-note'>This Space <strong>is an MCP server</strong> "
            f"(7 tools) at <code>{SPACE_MCP}</code>. The full DC Hub MCP — "
            f"{_tools_strong()}real-time grid telemetry, fiber, gas, "
            f"water, tax incentives, deal intelligence — is at "
            f"<code>{FULL_MCP}</code> (10 calls/day free anonymous; mint a free "
            f"key with its <code>claim_free_key</code> tool, or try it with zero "
            f"setup at <a href='https://dchub.cloud/playground'>dchub.cloud/playground</a>).</p>")
        gr.Markdown(
            f"<p class='dc-mcp-note'>Per-client setup recipes — Claude, Cursor, "
            f"VS Code, Cline, Windsurf, ChatGPT, Gemini, Perplexity, Grok, "
            f"DeepSeek Harness — are at "
            f"<a href='{API}/connect'>dchub.cloud/connect</a>, and the same list "
            f"in agent form at <a href='{API}/ai-agents'>dchub.cloud/ai-agents</a>.</p>")
        with gr.Tabs():
            with gr.Tab("Claude Code"):
                gr.Code(_claude_code_snippet(), language="shell", label="terminal")
            with gr.Tab("Desktop · Cursor"):
                gr.Code(JSON_SNIPPET, language="json", label="mcp config (JSON) — any MCP client")
            with gr.Tab("DeepSeek Harness"):
                gr.Code(DEEPSEEK_SNIPPET, language="yaml",
                        label="harness profile YAML — consumer DeepSeek chat has no MCP UI")
            with gr.Tab("smolagents"):
                gr.Code(SMOLAGENTS_SNIPPET, language="python", label="python — HF Agents / smolagents")

    with gr.Tab("⚡ Score a market"):
        inp = gr.Textbox(label="U.S. data-center market", value="Northern Virginia",
                         placeholder="Northern Virginia, Phoenix, Columbus, Cheyenne…",
                         info="One market name — get the verdict, score, cost, and time-to-power.")
        btn = gr.Button("Get DCPI score", variant="primary")
        out = gr.Markdown()
        btn.click(dcpi_score, inp, out)
        inp.submit(dcpi_score, inp, out, api_name=False)
        gr.Examples([["Northern Virginia"], ["Phoenix"], ["Columbus"],
                     ["Cheyenne"], ["Dallas"], ["Omaha"]], inp)
    with gr.Tab("🏆 Compare markets"):
        cinp = gr.Textbox(label="Markets (comma-separated)",
                          value="Northern Virginia, Phoenix, Cheyenne, Omaha, Dallas",
                          info="Up to 8 U.S. markets — ranked best power-availability first.")
        cbtn = gr.Button("Rank by power availability", variant="primary")
        cout = gr.Markdown()
        cbtn.click(compare_markets, cinp, cout)
        cinp.submit(compare_markets, cinp, cout, api_name=False)
        gr.Examples([["Northern Virginia, Phoenix, Cheyenne, Omaha, Dallas"],
                     ["Atlanta, Columbus, Tulsa"],
                     ["Dallas, Austin, San Antonio"]], cinp)
    with gr.Tab("🌍 Grid scoreboard"):
        gr.Markdown("Live ranked grids — US ISOs + GB, EU, Taiwan, Japan, South Korea, "
                    "Brazil: fuel mix, renewable share, and demand **right now**. "
                    "Greenest first.")
        gout = gr.Code(language="json", label="live scoreboard")
        gr.Button("Live grid scoreboard (greenest first)", variant="primary").click(
            grid_scoreboard, [], gout)
    with gr.Tab("🔎 Search facilities"):
        with gr.Row():
            s_q = gr.Textbox(label="Query", value="Ashburn",
                             placeholder="name / operator / location")
            s_c = gr.Textbox(label="Country (ISO-2)", value="US", placeholder="US, GB, SG…")
            s_s = gr.Textbox(label="State", placeholder="VA, TX…")
        with gr.Row():
            s_o = gr.Textbox(label="Operator", placeholder="Equinix, Digital Realty…")
            s_m = gr.Number(label="Min capacity (MW)", value=0)
            s_l = gr.Number(label="Limit", value=10)
        sout = gr.Code(language="json", label="facilities")
        gr.Button("Search facilities", variant="primary").click(
            search_facilities, [s_q, s_c, s_s, s_o, s_m, s_l], sout)
    with gr.Tab("📊 Rank markets"):
        with gr.Row():
            r_c = gr.Dropdown(["most_capacity", "cheapest_power", "most_operators",
                               "fastest"], value="most_capacity", label="Criteria")
            r_r = gr.Dropdown(["us", "global", "canada", "eu", "apac", "americas"],
                              value="us", label="Region")
            r_l = gr.Number(label="Limit", value=10)
        rout = gr.Code(language="json", label="ranking")
        gr.Button("Rank markets", variant="primary").click(
            rank_markets, [r_c, r_r, r_l], rout)
    with gr.Tab("🔌 Interconnection queue"):
        q_i = gr.Dropdown(["", "ERCOT", "PJM", "MISO", "CAISO", "SPP", "NYISO",
                           "ISONE"], value="", label="ISO",
                          info="How much capacity is waiting to connect. Empty = national overview.")
        qout = gr.Code(language="json", label="queue intelligence")
        gr.Button("Queue intelligence", variant="primary").click(
            interconnection_queue, [q_i], qout)
    with gr.Tab("💸 Hyperscaler deals"):
        d_l = gr.Number(label="Limit", value=10,
                        info="Most recent hyperscaler / AI-capex deals from the DC Hub tracker.")
        dout = gr.Code(language="json", label="deals")
        gr.Button("Recent AI-capex deals", variant="primary").click(
            hyperscaler_deals, [d_l], dout)

    gr.HTML(
        f"<div class='dc-foot'>Data: <a href='https://dchub.cloud'>DC Hub</a>, CC-BY-4.0 — "
        f"the live infrastructure data layer for AI agents · "
        f"<a href='{API}/playground'>playground</a> · "
        f"<a href='{API}/signup'>free key</a> · "
        f"<a href='{API}/connect'>connect</a> · "
        f"<a href='{API}/ai-agents'>ai-agents</a></div>")

if __name__ == "__main__":
    # mcp_server=True exposes all 7 tools as MCP tools at /gradio_api/mcp/sse —
    # usable by HF Agents, smolagents, and any MCP client.
    demo.launch(mcp_server=True)