spotgpu-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: spotgpu-mcp
3
+ Version: 0.1.0
4
+ Summary: SpotGPU MCP server — spot GPU rental prices via GET /v1/spot
5
+ Author: SpotGPU
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://spotgpu-api.fly.dev
8
+ Project-URL: Documentation, https://spotgpu-api.fly.dev/docs
9
+ Keywords: mcp,gpu,spot,pricing,vast,runpod
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: mcp<3,>=2
22
+ Requires-Dist: httpx>=0.27.0
23
+
24
+ # spotgpu-mcp
25
+
26
+ MCP (Model Context Protocol) server for [SpotGPU](https://spotgpu-api.fly.dev) spot GPU rental prices.
27
+
28
+ Exposes one tool: **`spot_price`** → `GET /v1/spot` (Vast + RunPod best + alts).
29
+
30
+ > **Credits ≠ rental.** SpotGPU API credits pay for price lookups.
31
+ > **`usd_per_hr` is the upstream provider rental price**, not a SpotGPU fee.
32
+
33
+ <!-- mcp-name: io.github.flowtestpro-svg/spotgpu-mcp -->
34
+
35
+ ## Install / run
36
+
37
+ Requires Python 3.10+ and a SpotGPU API key (`sk_…` from https://spotgpu-api.fly.dev).
38
+
39
+ ### One-shot (after PyPI publish)
40
+
41
+ ```bash
42
+ uvx spotgpu-mcp
43
+ ```
44
+
45
+ ### Local (this monorepo package)
46
+
47
+ ```bash
48
+ cd packages/mcp-spotgpu
49
+ uv sync # or: pip install -e .
50
+ export SPOTGPU_API_KEY=sk_…
51
+ spotgpu-mcp
52
+ ```
53
+
54
+ Env:
55
+
56
+ | Variable | Required | Default |
57
+ |----------|----------|---------|
58
+ | `SPOTGPU_API_KEY` | **yes** | — |
59
+ | `SPOTGPU_BASE_URL` | no | `https://spotgpu-api.fly.dev` |
60
+
61
+ Never hardcode keys. The server speaks MCP on **stdio** only (do not print to stdout).
62
+
63
+ ## Claude Desktop
64
+
65
+ Add to `claude_desktop_config.json`:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "spotgpu": {
71
+ "command": "uvx",
72
+ "args": ["spotgpu-mcp"],
73
+ "env": {
74
+ "SPOTGPU_API_KEY": "sk_…"
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ Local editable install instead of `uvx`:
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "spotgpu": {
87
+ "command": "uv",
88
+ "args": ["run", "--directory", "/path/to/spotgpu/packages/mcp-spotgpu", "spotgpu-mcp"],
89
+ "env": {
90
+ "SPOTGPU_API_KEY": "sk_…"
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ## Cursor
98
+
99
+ Add to `.cursor/mcp.json` (or user MCP config):
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "spotgpu": {
105
+ "command": "uvx",
106
+ "args": ["spotgpu-mcp"],
107
+ "env": {
108
+ "SPOTGPU_API_KEY": "sk_…"
109
+ }
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ ## Tool: `spot_price`
116
+
117
+ | Param | Type | Default | Notes |
118
+ |-------|------|---------|-------|
119
+ | `gpu` | string | required | e.g. `RTX_4090` |
120
+ | `qty` | int | `1` | ≥ 1 |
121
+ | `markets` | string | `"vast,runpod"` | comma-separated |
122
+ | `fresh` | bool | `false` | maps to `fresh=1` |
123
+
124
+ On success returns JSON text of the API body (`best.usd_per_hr`, `alts`, …).
125
+
126
+ ## Attribution
127
+
128
+ Requests send `X-SpotGPU-Client: mcp-spotgpu`.
129
+
130
+ ## Not published yet
131
+
132
+ PyPI / Official MCP Registry / Smithery / Glama publish is deferred until Biz Bot approval. `server.json` and `glama.json` stubs are in this package for later.
@@ -0,0 +1,109 @@
1
+ # spotgpu-mcp
2
+
3
+ MCP (Model Context Protocol) server for [SpotGPU](https://spotgpu-api.fly.dev) spot GPU rental prices.
4
+
5
+ Exposes one tool: **`spot_price`** → `GET /v1/spot` (Vast + RunPod best + alts).
6
+
7
+ > **Credits ≠ rental.** SpotGPU API credits pay for price lookups.
8
+ > **`usd_per_hr` is the upstream provider rental price**, not a SpotGPU fee.
9
+
10
+ <!-- mcp-name: io.github.flowtestpro-svg/spotgpu-mcp -->
11
+
12
+ ## Install / run
13
+
14
+ Requires Python 3.10+ and a SpotGPU API key (`sk_…` from https://spotgpu-api.fly.dev).
15
+
16
+ ### One-shot (after PyPI publish)
17
+
18
+ ```bash
19
+ uvx spotgpu-mcp
20
+ ```
21
+
22
+ ### Local (this monorepo package)
23
+
24
+ ```bash
25
+ cd packages/mcp-spotgpu
26
+ uv sync # or: pip install -e .
27
+ export SPOTGPU_API_KEY=sk_…
28
+ spotgpu-mcp
29
+ ```
30
+
31
+ Env:
32
+
33
+ | Variable | Required | Default |
34
+ |----------|----------|---------|
35
+ | `SPOTGPU_API_KEY` | **yes** | — |
36
+ | `SPOTGPU_BASE_URL` | no | `https://spotgpu-api.fly.dev` |
37
+
38
+ Never hardcode keys. The server speaks MCP on **stdio** only (do not print to stdout).
39
+
40
+ ## Claude Desktop
41
+
42
+ Add to `claude_desktop_config.json`:
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "spotgpu": {
48
+ "command": "uvx",
49
+ "args": ["spotgpu-mcp"],
50
+ "env": {
51
+ "SPOTGPU_API_KEY": "sk_…"
52
+ }
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ Local editable install instead of `uvx`:
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "spotgpu": {
64
+ "command": "uv",
65
+ "args": ["run", "--directory", "/path/to/spotgpu/packages/mcp-spotgpu", "spotgpu-mcp"],
66
+ "env": {
67
+ "SPOTGPU_API_KEY": "sk_…"
68
+ }
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ ## Cursor
75
+
76
+ Add to `.cursor/mcp.json` (or user MCP config):
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "spotgpu": {
82
+ "command": "uvx",
83
+ "args": ["spotgpu-mcp"],
84
+ "env": {
85
+ "SPOTGPU_API_KEY": "sk_…"
86
+ }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ ## Tool: `spot_price`
93
+
94
+ | Param | Type | Default | Notes |
95
+ |-------|------|---------|-------|
96
+ | `gpu` | string | required | e.g. `RTX_4090` |
97
+ | `qty` | int | `1` | ≥ 1 |
98
+ | `markets` | string | `"vast,runpod"` | comma-separated |
99
+ | `fresh` | bool | `false` | maps to `fresh=1` |
100
+
101
+ On success returns JSON text of the API body (`best.usd_per_hr`, `alts`, …).
102
+
103
+ ## Attribution
104
+
105
+ Requests send `X-SpotGPU-Client: mcp-spotgpu`.
106
+
107
+ ## Not published yet
108
+
109
+ PyPI / Official MCP Registry / Smithery / Glama publish is deferred until Biz Bot approval. `server.json` and `glama.json` stubs are in this package for later.
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "spotgpu-mcp"
7
+ version = "0.1.0"
8
+ description = "SpotGPU MCP server — spot GPU rental prices via GET /v1/spot"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "SpotGPU" }]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Software Development :: Libraries",
22
+ "Typing :: Typed",
23
+ ]
24
+ keywords = ["mcp", "gpu", "spot", "pricing", "vast", "runpod"]
25
+ dependencies = [
26
+ "mcp>=2,<3",
27
+ "httpx>=0.27.0",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://spotgpu-api.fly.dev"
32
+ Documentation = "https://spotgpu-api.fly.dev/docs"
33
+
34
+ [project.scripts]
35
+ spotgpu-mcp = "spotgpu_mcp.server:main"
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
39
+ include = ["spotgpu_mcp*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """SpotGPU MCP thin client (stdio)."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,140 @@
1
+ """Thin httpx client for SpotGPU GET /v1/spot. No app-internal imports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ DEFAULT_BASE_URL = "https://spotgpu-api.fly.dev"
12
+ CLIENT_HEADER = "mcp-spotgpu"
13
+ TIMEOUT_S = 60.0
14
+
15
+
16
+ class SpotGPUError(Exception):
17
+ """Raised for API/config failures; message is safe for MCP tool errors."""
18
+
19
+ def __init__(self, message: str) -> None:
20
+ super().__init__(message)
21
+ self.message = message
22
+
23
+
24
+ def _base_url() -> str:
25
+ return (os.environ.get("SPOTGPU_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
26
+
27
+
28
+ def _api_key() -> str:
29
+ key = os.environ.get("SPOTGPU_API_KEY")
30
+ if not key or not key.strip():
31
+ raise SpotGPUError(
32
+ "Server misconfigured: SPOTGPU_API_KEY not set. "
33
+ "Set it in your MCP client env (Claude Desktop / Cursor)."
34
+ )
35
+ return key.strip()
36
+
37
+
38
+ def _detail_snippet(body: Any) -> str:
39
+ """Extract a short human detail without leaking secrets."""
40
+ if body is None:
41
+ return ""
42
+ if isinstance(body, str):
43
+ text = body.strip()
44
+ return f" — {text[:300]}" if text else ""
45
+ if isinstance(body, dict):
46
+ for k in ("message", "detail", "error"):
47
+ v = body.get(k)
48
+ if isinstance(v, str) and v.strip():
49
+ return f" — {v.strip()[:300]}"
50
+ if isinstance(v, list) and v:
51
+ return f" — {json.dumps(v)[:300]}"
52
+ # FastAPI validation often nests under detail
53
+ detail = body.get("detail")
54
+ if detail is not None:
55
+ try:
56
+ return f" — {json.dumps(detail)[:300]}"
57
+ except (TypeError, ValueError):
58
+ return f" — {str(detail)[:300]}"
59
+ try:
60
+ return f" — {json.dumps(body)[:300]}"
61
+ except (TypeError, ValueError):
62
+ return ""
63
+
64
+
65
+ def _map_http_error(status: int, body: Any, base: str) -> SpotGPUError:
66
+ if status == 401:
67
+ return SpotGPUError(
68
+ "Invalid or missing API key — check SPOTGPU_API_KEY"
69
+ + _detail_snippet(body)
70
+ )
71
+ if status == 402:
72
+ return SpotGPUError(
73
+ f"Insufficient credits — top up at {base}/topup or POST /v1/checkout"
74
+ + _detail_snippet(body)
75
+ )
76
+ if status == 404:
77
+ return SpotGPUError(
78
+ "No offers / unknown GPU" + _detail_snippet(body)
79
+ )
80
+ if status == 422:
81
+ return SpotGPUError("Bad params" + _detail_snippet(body))
82
+ if status in (502, 503):
83
+ return SpotGPUError(
84
+ "Upstream markets unavailable — retry later" + _detail_snippet(body)
85
+ )
86
+ return SpotGPUError(f"SpotGPU API HTTP {status}" + _detail_snippet(body))
87
+
88
+
89
+ def fetch_spot(
90
+ *,
91
+ gpu: str,
92
+ qty: int = 1,
93
+ markets: str = "vast,runpod",
94
+ fresh: bool = False,
95
+ ) -> dict[str, Any]:
96
+ """
97
+ GET {BASE}/v1/spot with Bearer auth.
98
+
99
+ Maps fresh=True → query fresh=1. Returns parsed JSON body on 200.
100
+ """
101
+ base = _base_url()
102
+ key = _api_key()
103
+ params: dict[str, str | int] = {
104
+ "gpu": gpu,
105
+ "qty": qty,
106
+ "markets": markets,
107
+ }
108
+ if fresh:
109
+ params["fresh"] = 1
110
+
111
+ headers = {
112
+ "Authorization": f"Bearer {key}",
113
+ "Accept": "application/json",
114
+ "X-SpotGPU-Client": CLIENT_HEADER,
115
+ }
116
+ url = f"{base}/v1/spot"
117
+
118
+ try:
119
+ with httpx.Client(timeout=TIMEOUT_S) as client:
120
+ resp = client.get(url, params=params, headers=headers)
121
+ except httpx.TimeoutException as exc:
122
+ raise SpotGPUError("Connection failed to SpotGPU API (timeout)") from exc
123
+ except httpx.RequestError as exc:
124
+ raise SpotGPUError("Connection failed to SpotGPU API") from exc
125
+
126
+ if resp.status_code == 200:
127
+ try:
128
+ data = resp.json()
129
+ except json.JSONDecodeError as exc:
130
+ raise SpotGPUError("SpotGPU API returned non-JSON body") from exc
131
+ if not isinstance(data, dict):
132
+ raise SpotGPUError("SpotGPU API returned unexpected JSON shape")
133
+ return data
134
+
135
+ body: Any
136
+ try:
137
+ body = resp.json()
138
+ except json.JSONDecodeError:
139
+ body = resp.text
140
+ raise _map_http_error(resp.status_code, body, base)
@@ -0,0 +1,61 @@
1
+ """SpotGPU MCP server (stdio only). Never write non-protocol bytes to stdout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import sys
8
+
9
+ from mcp.server import MCPServer
10
+
11
+ from spotgpu_mcp.client import SpotGPUError, fetch_spot
12
+
13
+ # Stdio MCP: logging must go to stderr only
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format="%(levelname)s %(name)s: %(message)s",
17
+ stream=sys.stderr,
18
+ )
19
+ log = logging.getLogger("spotgpu_mcp")
20
+
21
+ mcp = MCPServer("spotgpu")
22
+
23
+
24
+ @mcp.tool()
25
+ def spot_price(
26
+ gpu: str,
27
+ qty: int = 1,
28
+ markets: str = "vast,runpod",
29
+ fresh: bool = False,
30
+ ) -> str:
31
+ """Get SpotGPU spot rental prices (best + alts) for a GPU type.
32
+
33
+ Returns JSON from GET /v1/spot including best.usd_per_hr (upstream
34
+ provider rental price, not a SpotGPU fee). Credits are for API calls,
35
+ not GPU rental.
36
+
37
+ Args:
38
+ gpu: GPU type, e.g. RTX_4090, RTX_3090, RTX_A6000
39
+ qty: Number of GPUs (>= 1). Default 1.
40
+ markets: Comma-separated markets. Default "vast,runpod".
41
+ fresh: If true, bypass cache (query fresh=1). Default false.
42
+ """
43
+ if qty < 1:
44
+ raise SpotGPUError("Bad params — qty must be >= 1")
45
+ try:
46
+ data = fetch_spot(gpu=gpu, qty=qty, markets=markets, fresh=fresh)
47
+ except SpotGPUError:
48
+ raise
49
+ except Exception as exc: # noqa: BLE001 — surface unexpected as tool error
50
+ log.exception("spot_price unexpected failure")
51
+ raise SpotGPUError(f"SpotGPU client error: {exc}") from exc
52
+ return json.dumps(data, indent=2)
53
+
54
+
55
+ def main() -> None:
56
+ """CLI entry: spotgpu-mcp — stdio transport only."""
57
+ mcp.run(transport="stdio")
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: spotgpu-mcp
3
+ Version: 0.1.0
4
+ Summary: SpotGPU MCP server — spot GPU rental prices via GET /v1/spot
5
+ Author: SpotGPU
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://spotgpu-api.fly.dev
8
+ Project-URL: Documentation, https://spotgpu-api.fly.dev/docs
9
+ Keywords: mcp,gpu,spot,pricing,vast,runpod
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: mcp<3,>=2
22
+ Requires-Dist: httpx>=0.27.0
23
+
24
+ # spotgpu-mcp
25
+
26
+ MCP (Model Context Protocol) server for [SpotGPU](https://spotgpu-api.fly.dev) spot GPU rental prices.
27
+
28
+ Exposes one tool: **`spot_price`** → `GET /v1/spot` (Vast + RunPod best + alts).
29
+
30
+ > **Credits ≠ rental.** SpotGPU API credits pay for price lookups.
31
+ > **`usd_per_hr` is the upstream provider rental price**, not a SpotGPU fee.
32
+
33
+ <!-- mcp-name: io.github.flowtestpro-svg/spotgpu-mcp -->
34
+
35
+ ## Install / run
36
+
37
+ Requires Python 3.10+ and a SpotGPU API key (`sk_…` from https://spotgpu-api.fly.dev).
38
+
39
+ ### One-shot (after PyPI publish)
40
+
41
+ ```bash
42
+ uvx spotgpu-mcp
43
+ ```
44
+
45
+ ### Local (this monorepo package)
46
+
47
+ ```bash
48
+ cd packages/mcp-spotgpu
49
+ uv sync # or: pip install -e .
50
+ export SPOTGPU_API_KEY=sk_…
51
+ spotgpu-mcp
52
+ ```
53
+
54
+ Env:
55
+
56
+ | Variable | Required | Default |
57
+ |----------|----------|---------|
58
+ | `SPOTGPU_API_KEY` | **yes** | — |
59
+ | `SPOTGPU_BASE_URL` | no | `https://spotgpu-api.fly.dev` |
60
+
61
+ Never hardcode keys. The server speaks MCP on **stdio** only (do not print to stdout).
62
+
63
+ ## Claude Desktop
64
+
65
+ Add to `claude_desktop_config.json`:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "spotgpu": {
71
+ "command": "uvx",
72
+ "args": ["spotgpu-mcp"],
73
+ "env": {
74
+ "SPOTGPU_API_KEY": "sk_…"
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ Local editable install instead of `uvx`:
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "spotgpu": {
87
+ "command": "uv",
88
+ "args": ["run", "--directory", "/path/to/spotgpu/packages/mcp-spotgpu", "spotgpu-mcp"],
89
+ "env": {
90
+ "SPOTGPU_API_KEY": "sk_…"
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ## Cursor
98
+
99
+ Add to `.cursor/mcp.json` (or user MCP config):
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "spotgpu": {
105
+ "command": "uvx",
106
+ "args": ["spotgpu-mcp"],
107
+ "env": {
108
+ "SPOTGPU_API_KEY": "sk_…"
109
+ }
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ ## Tool: `spot_price`
116
+
117
+ | Param | Type | Default | Notes |
118
+ |-------|------|---------|-------|
119
+ | `gpu` | string | required | e.g. `RTX_4090` |
120
+ | `qty` | int | `1` | ≥ 1 |
121
+ | `markets` | string | `"vast,runpod"` | comma-separated |
122
+ | `fresh` | bool | `false` | maps to `fresh=1` |
123
+
124
+ On success returns JSON text of the API body (`best.usd_per_hr`, `alts`, …).
125
+
126
+ ## Attribution
127
+
128
+ Requests send `X-SpotGPU-Client: mcp-spotgpu`.
129
+
130
+ ## Not published yet
131
+
132
+ PyPI / Official MCP Registry / Smithery / Glama publish is deferred until Biz Bot approval. `server.json` and `glama.json` stubs are in this package for later.
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/spotgpu_mcp/__init__.py
4
+ src/spotgpu_mcp/client.py
5
+ src/spotgpu_mcp/server.py
6
+ src/spotgpu_mcp.egg-info/PKG-INFO
7
+ src/spotgpu_mcp.egg-info/SOURCES.txt
8
+ src/spotgpu_mcp.egg-info/dependency_links.txt
9
+ src/spotgpu_mcp.egg-info/entry_points.txt
10
+ src/spotgpu_mcp.egg-info/requires.txt
11
+ src/spotgpu_mcp.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ spotgpu-mcp = spotgpu_mcp.server:main
@@ -0,0 +1,2 @@
1
+ mcp<3,>=2
2
+ httpx>=0.27.0
@@ -0,0 +1 @@
1
+ spotgpu_mcp