snowbll-mcp 0.1.0 → 0.2.1

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.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Snowbll
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Snowbll
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,234 +1,122 @@
1
- # Snowbll MCP
2
-
3
- **Snowbll gives AI agents deep Gaming Behaviour Intelligence.**
4
-
5
- > Not another game recommender. A behaviour intelligence layer for gaming agents.
6
-
7
- Snowbll MCP is a [Model Context Protocol](https://modelcontextprotocol.io) server
8
- that exposes Snowbll's gaming-behaviour intelligence as tools any AI agent can
9
- call Claude, Cursor, Windsurf, OpenAI Agents SDK, LangChain, CrewAI, AutoGen,
10
- the Vercel AI SDK, and custom TypeScript/Python agents.
11
-
12
- It helps agents understand how a gamer actually plays: their playstyle DNA,
13
- gaming persona, session patterns, completion behavior, purchase behavior,
14
- genre mismatches, game-fit prediction, and likely next best game.
15
-
16
- ## What this is (and is not)
17
-
18
- ```
19
- AI Agent
20
- Snowbll MCP Server (stdio)
21
- → Snowbll API or mock data
22
- Gaming Behaviour Intelligence
23
- ```
24
-
25
- Snowbll MCP is **not** the agent. It is a **tool provider**. The agent decides
26
- when to call a tool; Snowbll MCP returns structured behaviour intelligence as
27
- JSON. The first version ships with realistic **mock data** so you can build and
28
- test integrations immediately, with no backend required.
29
-
30
- ## Install
31
-
32
- ```bash
33
- npm install
34
- npm run build
35
- ```
36
-
37
- ## Run locally
38
-
39
- Run the server in mock mode (the default — no API key needed):
40
-
41
- ```bash
42
- npm run dev
43
- ```
44
-
45
- You should see, on stderr:
46
-
47
- ```
48
- [snowbll-mcp] running on stdio — data mode: mock
49
- ```
50
-
51
- The server speaks MCP over stdio, so it's meant to be launched by an MCP client
52
- (Claude Desktop, Cursor, etc.) rather than used interactively. To smoke-test the
53
- built binary directly:
54
-
55
- ```bash
56
- npm run build
57
- node dist/index.js
58
- ```
59
-
60
- ## Connect it to Claude / Cursor (local development)
61
-
62
- After `npm run build`, point your MCP client at the built file. Use an absolute
63
- path to `dist/index.js`.
64
-
65
- ```json
66
- {
67
- "mcpServers": {
68
- "snowbll": {
69
- "command": "node",
70
- "args": ["C:/PATH/TO/snowbll-mcp/dist/index.js"],
71
- "env": {
72
- "SNOWBLL_USE_MOCK": "true"
73
- }
74
- }
75
- }
76
- }
77
- ```
78
-
79
- - **Claude Desktop:** add this to `claude_desktop_config.json`
80
- (Settings Developer → Edit Config), then restart Claude Desktop.
81
- - **Cursor / Windsurf:** add it to the editor's MCP settings (`mcp.json`).
82
-
83
- ### Future: install from npm
84
-
85
- Once Snowbll MCP is published and you have an API key, no local build is needed:
86
-
87
- ```json
88
- {
89
- "mcpServers": {
90
- "snowbll": {
91
- "command": "npx",
92
- "args": ["-y", "snowbll-mcp"],
93
- "env": {
94
- "SNOWBLL_API_KEY": "snb_YOUR_API_KEY"
95
- }
96
- }
97
- }
98
- }
99
- ```
100
-
101
- ## Configuration
102
-
103
- | Variable | Default | Description |
104
- | ------------------ | ----------------------------- | --------------------------------------------------------------------------- |
105
- | `SNOWBLL_USE_MOCK` | `true` | Use local mock intelligence. Set to `false` to call the real Snowbll API. |
106
- | `SNOWBLL_API_URL` | `https://api.snowbll.com/v1` | Base URL of the Snowbll API (live mode only). |
107
- | `SNOWBLL_API_KEY` | _(none)_ | Required when `SNOWBLL_USE_MOCK=false`. Format `snb_...`. |
108
- | `SNOWBLL_API_TIMEOUT_MS` | `10000` | Per-request timeout (live mode). Hung requests abort and surface an error. |
109
- | `SNOWBLL_API_RETRIES` | `2` | Retries after the first attempt for transient errors (timeout, 429, 5xx). |
110
-
111
- Mock mode is the default: the server runs with zero configuration and never
112
- needs an API key. In live mode (`SNOWBLL_USE_MOCK=false`), a missing
113
- `SNOWBLL_API_KEY` produces a clear, actionable error. Live requests time out and
114
- retry transient failures (429/5xx) with exponential backoff; non-retryable 4xx
115
- errors fail fast.
116
-
117
- ## Tests
118
-
119
- ```bash
120
- npm test
121
- ```
122
-
123
- Runs the suite via Node's built-in test runner (`tsx test/all.test.ts`): mock
124
- intelligence logic, output-schema round-trips, client mock/live routing, and the
125
- HTTP layer's timeout/retry behaviour (against a local server). No network.
126
-
127
- ## Common workflows
128
-
129
- **Understand a player**
130
- 1. `get_player_overview` — high-level identity and current objective.
131
- 2. `analyze_player_behaviour` — full behavioural analysis (`depth: "deep"` for correlations and risks).
132
- 3. `get_persona_cards` / `get_playstyle_dna` — evidence-backed detail.
133
-
134
- **Explain taste and behaviour**
135
- - `explain_taste_pattern` with a question like _"Why do I abandon open-world RPGs?"_
136
- - `detect_genre_mismatch` to surface where stated taste diverges from observed behaviour.
137
-
138
- **Predict game fit**
139
- - `predict_game_fit` for one title.
140
- - `compare_games_for_player` to rank 2–5 candidates.
141
-
142
- **Recommend what to play next**
143
- - `recommend_games` with an objective (`likely_to_finish`, `hidden_gems`, `deep_systems`, `comfort_games`, `short_sessions`, `high_progression`).
144
-
145
- ## Tool catalog
146
-
147
- | Tool | Input | What it returns |
148
- | ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------- |
149
- | `get_player_overview` | _none_ | Totals, persona match, current objective, identity summary. |
150
- | `get_playstyle_dna` | _none_ | Playstyle traits with scores and tiers, plus the player's unique edge. |
151
- | `get_persona_cards` | _none_ | Persona cards with rarity, level, progress, and evidence. |
152
- | `get_activity_patterns` | _none_ | Active hours, session-length distribution, platform split, play rhythm. |
153
- | `get_completion_behavior` | _none_ | Completion rate, strong/weak categories, drop-off signals. |
154
- | `get_purchase_behavior` | `includeSensitiveSignals?` | Purchase aggregates and buy-vs-play mismatch. |
155
- | `detect_genre_mismatch` | `includePurchaseSignals?`, `includeCompletionSignals?` | Where claimed/purchased taste diverges from observed behaviour. |
156
- | `analyze_player_behaviour` | `depth?`, `includeEvidence?` | Full behavioural analysis with evidence and interpretation. |
157
- | `explain_taste_pattern` | `topic` | Explanation, supporting signals, and actionable advice for a topic. |
158
- | `predict_game_fit` | `gameTitle`, `platform?` | Fit score, finish likelihood, drop-off risk, and reasoning for one game. |
159
- | `compare_games_for_player` | `games[2..5]` | Ranked comparison with a best pick. |
160
- | `recommend_games` | `objective?`, `limit?` | Behaviour-fit game recommendations for a chosen objective. |
161
- | `crosscheck_recommendations` | `objective?`, `limit?` | Games validated by TWO engines (consensus + referee) with an agreement score. |
162
-
163
- ## Resources
164
-
165
- Attach a player's Snowbll context directly (read-only) instead of calling several tools:
166
-
167
- | Resource URI | Contents |
168
- | -------------------------------- | ------------------------------------------------------- |
169
- | `snowbll://player/profile` | Combined overview + playstyle DNA + personas + activity |
170
- | `snowbll://player/overview` | High-level identity and current objective |
171
- | `snowbll://player/playstyle-dna` | Playstyle traits with scores and tiers |
172
- | `snowbll://player/personas` | Persona cards |
173
-
174
- ## Prompts
175
-
176
- Ready-made templates, surfaced as slash-commands in Claude Desktop / Cursor:
177
-
178
- | Prompt | Arguments | What it does |
179
- | ------------------- | ------------ | -------------------------------------------- |
180
- | `understand-player` | — | Build a full profile of the player |
181
- | `what-to-play` | `objective?` | Recommend the next game and explain why |
182
- | `should-i-play` | `gameTitle` | Predict fit and finish likelihood for a game |
183
- | `compare-games` | `games` | Rank 2–5 games by behavioural fit |
184
-
185
- > **Structured output:** the behaviour-intelligence tools return validated
186
- > `structuredContent` (machine-readable JSON) alongside the text result.
187
-
188
- ## Privacy
189
-
190
- By default, Snowbll MCP tools return **aggregate behaviour intelligence** — not
191
- raw session logs, raw purchase data, private posts, or friend data.
192
- `get_purchase_behavior` only exposes raw, purchase-level detail when explicitly
193
- asked via `includeSensitiveSignals: true`; otherwise it returns aggregates only.
194
-
195
- ## Extending to a real backend
196
-
197
- `src/client.ts` is the single seam between the MCP tools and the data source.
198
- Each tool maps to one `client.call(endpoint, payload, mockFn)`:
199
-
200
- - In **mock mode**, `mockFn()` (from `src/mockData.ts`) returns the data.
201
- - In **live mode**, the same call `POST`s `payload` to `SNOWBLL_API_URL + endpoint`
202
- with `Authorization: Bearer <SNOWBLL_API_KEY>` and returns the JSON response.
203
-
204
- To wire up the real Snowbll API, implement those endpoints to match the response
205
- shapes in `src/mockData.ts` (exported as TypeScript interfaces) and set
206
- `SNOWBLL_USE_MOCK=false`. No changes to `index.ts` are required.
207
-
208
- ## Project layout
209
-
210
- ```
211
- snowbll-mcp/
212
- package.json
213
- tsconfig.json
214
- README.md
215
- src/
216
- index.ts # MCP server: registers tools, resources & prompts over stdio
217
- client.ts # game data layer: mock-by-default, future real-API client
218
- communityClient.ts # community/forum data layer (Community Recommendation Engine)
219
- mockData.ts # typed mock dataset + helpers (game response shapes)
220
- schemas.ts # zod output schemas (structured output) + type-sync guard
221
- resources.ts # MCP resources (player snapshots)
222
- prompts.ts # MCP prompts (slash-command templates)
223
- http.ts # HTTP helper: timeout, retry, typed errors
224
- test/ # node:test suite (npm test)
225
- ```
226
-
227
- ## Tech stack
228
-
229
- TypeScript (strict, ESM) · Node.js ≥ 18 · `@modelcontextprotocol/sdk` · `zod` ·
230
- stdio MCP transport.
231
-
232
- ## License
233
-
234
- MIT
1
+ # Snowbll MCP + CLI
2
+
3
+ **Search the Snowbll game catalog, read your gaming persona, and follow Forge campaigns — from any MCP-capable AI agent or your terminal.**
4
+
5
+ Everything in this package talks to the **live Snowbll API** (`https://www.snowbll.com`). There is no mock data.
6
+
7
+ Status: the search and Forge tools hit live public endpoints; the MCP servers, API keys, and the keyed v1 endpoints are in **developer preview** — names, fields, auth, and quotas may change before launch.
8
+
9
+ Two binaries ship in one package:
10
+
11
+ | Binary | What it is |
12
+ | --- | --- |
13
+ | `snowbll-mcp` | [Model Context Protocol](https://modelcontextprotocol.io) server over stdio — launched by agents (Claude, Cursor, Hermes, …) |
14
+ | `snowbll` | CLI for humans — search, persona, Forge, key management |
15
+
16
+ There is also a **hosted remote MCP server (developer preview)** at `https://www.snowbll.com/api/mcp` (Streamable HTTP) for clients that connect to URLs instead of launching processes — ChatGPT connectors, Claude custom connectors, Hermes remote MCP. It is an **OAuth 2.1 protected resource**: MCP clients start the sign-in flow automatically on connect (no manual key needed); persona access uses the `persona.read` scope.
17
+
18
+ ## Tools
19
+
20
+ The MCP surface is deliberately read-only: tools recommend, explain, and retrieve. Nothing here buys games, backs campaigns, or moves money.
21
+
22
+ | Tool | What it does | Needs API key? |
23
+ | --- | --- | --- |
24
+ | `search_games` | Natural-language catalog search with ranked candidates + reasons | No |
25
+ | `get_game` | Full metadata for one game, incl. human-verification status | No |
26
+ | `get_persona` | The key owner's gaming persona — a playstyle + taste summary from their library and playtime (consent-scoped) | Yes — data visible from the **Observer** rank up; free accounts get a locked marker |
27
+ | `list_forge_campaigns` | Forge campaigns with live funding totals | No |
28
+ | `get_forge_campaign` | One campaign: tiers, stretch goals, progress | No |
29
+
30
+ Honesty model: `verification` is `verified` only after third-party human testing; quality fields (`graphicsFidelity`, `economyComplexity`) are `null` until a human rated them. AI recommends, humans judge.
31
+
32
+ ## Quick start (agents)
33
+
34
+ ### Claude Code
35
+
36
+ Hosted (remote, OAuth — no install):
37
+
38
+ ```bash
39
+ claude mcp add --transport http snowbll https://www.snowbll.com/api/mcp
40
+ ```
41
+
42
+ Then run `/mcp` and complete the OAuth sign-in when prompted — no manual key needed. Persona access uses the `persona.read` scope.
43
+
44
+ Or run it locally over stdio (uses `SNOWBLL_API_KEY` if set — see below):
45
+
46
+ ```bash
47
+ claude mcp add snowbll -- npx -y snowbll-mcp
48
+ ```
49
+
50
+ ### Claude Desktop / Cursor / Windsurf / Hermes (stdio)
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "snowbll": {
56
+ "command": "npx",
57
+ "args": ["-y", "snowbll-mcp"],
58
+ "env": { "SNOWBLL_API_KEY": "sb_YOUR_API_KEY" }
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ Omit the `env` block to run keyless (search, game lookups, and Forge tools still work — only `get_persona` needs a key).
65
+
66
+ ### ChatGPT (remote connector — no install)
67
+
68
+ 1. Settings → Connectors (requires a plan with connectors/developer mode).
69
+ 2. Add a custom connector with URL `https://www.snowbll.com/api/mcp`, then complete the OAuth sign-in when prompted — no manual key needed. Persona access uses the `persona.read` scope.
70
+ 3. Ask: *"Use Snowbll to find an economy sim like Lemonade Tycoon with a deep economy."*
71
+
72
+ The remote server also implements OpenAI's `search` / `fetch` connector contract, so it works in ChatGPT deep research too.
73
+
74
+ ### Claude (claude.ai custom connector / remote)
75
+
76
+ Add a custom connector with URL `https://www.snowbll.com/api/mcp` and complete the OAuth sign-in when prompted — no manual key needed. The catalog and Forge tools (`search_games`, `get_game`, `list_forge_campaigns`, `get_forge_campaign`) are free once connected; `get_persona` is consent-scoped via `persona.read`.
77
+
78
+ ### Verify any setup
79
+
80
+ Ask the agent:
81
+
82
+ > Use search_games to find: "economy sim like Lemonade Tycoon with a deep economy"
83
+
84
+ A working setup returns a short candidate list, each with `reasons`.
85
+
86
+ ## Quick start (humans)
87
+
88
+ ```bash
89
+ npm install -g snowbll-mcp
90
+
91
+ snowbll search "cozy farming game with deep crafting"
92
+ snowbll forge # campaigns on The Forge
93
+ snowbll login # mint + store an API key (email+password account)
94
+ snowbll whoami
95
+ snowbll persona # your gaming persona
96
+ snowbll setup # wizard + ready-to-paste agent configs
97
+ ```
98
+
99
+ ## API keys (developer preview)
100
+
101
+ Keys (`sb_…`) belong to Snowbll accounts and are hashed at rest — the plaintext is shown once.
102
+
103
+ - `snowbll login` — sign in with an email+password Snowbll account; mints and stores a key in `~/.snowbll/config.json`.
104
+ - Accounts created with Google/Discord have no password: ask the Snowbll team for a preview key, then `snowbll setup` to paste it.
105
+ - `snowbll keys list|create|revoke` manages keys; `SNOWBLL_API_KEY` env always wins over the stored key.
106
+
107
+ Keep keys out of browser code, public repos, and shared transcripts.
108
+
109
+ ## Configuration
110
+
111
+ | Env var | Default | Purpose |
112
+ | --- | --- | --- |
113
+ | `SNOWBLL_API_KEY` | | Developer-preview API key |
114
+ | `SNOWBLL_API_BASE` | `https://www.snowbll.com` | API origin (point at a local dev server while developing) |
115
+
116
+ ## REST instead?
117
+
118
+ Everything the tools do is plain HTTP — `POST https://www.snowbll.com/api/search` is live with no key. Full docs: <https://www.snowbll.com/docs>.
119
+
120
+ ## License
121
+
122
+ MIT
package/dist/api.js ADDED
@@ -0,0 +1,138 @@
1
+ import { getApiBase, getApiKey, getSupabaseAnonKey, getSupabaseUrl } from "./config.js";
2
+ /**
3
+ * Thin client for the live Snowbll API.
4
+ *
5
+ * Two surfaces, as documented at https://www.snowbll.com/docs:
6
+ * - public endpoints (no key): POST /api/search, GET /api/forge/campaigns[/:id]
7
+ * - keyed v1 (developer preview): /api/v1/* with `Authorization: Bearer sb_...`
8
+ *
9
+ * Methods prefer the keyed surface when a key is configured and fall back to
10
+ * the public one when the data is public anyway, so search and Forge browsing
11
+ * work with zero setup.
12
+ */
13
+ export class SnowbllApiError extends Error {
14
+ status;
15
+ constructor(status, message) {
16
+ super(message);
17
+ this.status = status;
18
+ this.name = "SnowbllApiError";
19
+ }
20
+ }
21
+ async function request(path, options = {}) {
22
+ const { method = "GET", body, key, timeoutMs = 30_000 } = options;
23
+ const controller = new AbortController();
24
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
25
+ let res;
26
+ try {
27
+ res = await fetch(`${getApiBase()}${path}`, {
28
+ method,
29
+ headers: {
30
+ "content-type": "application/json",
31
+ ...(key ? { authorization: `Bearer ${key}` } : {}),
32
+ },
33
+ body: body === undefined ? undefined : JSON.stringify(body),
34
+ signal: controller.signal,
35
+ });
36
+ }
37
+ catch (err) {
38
+ clearTimeout(timer);
39
+ if (controller.signal.aborted)
40
+ throw new SnowbllApiError(0, `Request timed out after ${timeoutMs}ms.`);
41
+ throw new SnowbllApiError(0, `Network error: ${err instanceof Error ? err.message : String(err)}`);
42
+ }
43
+ clearTimeout(timer);
44
+ let json = null;
45
+ try {
46
+ json = await res.json();
47
+ }
48
+ catch {
49
+ // Non-JSON body — fall through to the status check.
50
+ }
51
+ if (!res.ok) {
52
+ const msg = json && typeof json === "object" && typeof json.error === "string"
53
+ ? json.error
54
+ : `HTTP ${res.status}`;
55
+ throw new SnowbllApiError(res.status, msg);
56
+ }
57
+ return json;
58
+ }
59
+ const NEEDS_KEY = "This needs an API key. Run `snowbll login` (email+password account) or `snowbll setup` to paste one — see https://www.snowbll.com/docs.";
60
+ function requireKey() {
61
+ const key = getApiKey();
62
+ if (!key)
63
+ throw new SnowbllApiError(401, NEEDS_KEY);
64
+ return key;
65
+ }
66
+ // ─── Catalog ────────────────────────────────────────────────────────────────
67
+ export async function searchGames(query, limit = 5) {
68
+ const key = getApiKey();
69
+ if (key)
70
+ return request("/api/v1/search", { method: "POST", body: { query, limit }, key });
71
+ // The public search endpoint ignores `limit` (returns up to 5) — trim client-side
72
+ // so keyless callers still get what they asked for.
73
+ const data = (await request("/api/search", { method: "POST", body: { query } }));
74
+ if (Array.isArray(data.results) && limit < data.results.length) {
75
+ return { ...data, results: data.results.slice(0, Math.max(1, Math.floor(limit))) };
76
+ }
77
+ return data;
78
+ }
79
+ export function getGame(gameId) {
80
+ const id = encodeURIComponent(gameId);
81
+ const key = getApiKey();
82
+ // Game detail is public, recommendation-grade data: use the keyed v1 endpoint
83
+ // when a key is set, else the public keyless one — so get_game works with zero
84
+ // setup, matching the hosted MCP server (only get_persona is a paid tool).
85
+ if (key)
86
+ return request(`/api/v1/games/${id}`, { key });
87
+ return request(`/api/games/${id}`);
88
+ }
89
+ export function listGames(limit = 20, offset = 0) {
90
+ return request(`/api/v1/games?limit=${limit}&offset=${offset}`, { key: requireKey() });
91
+ }
92
+ // ─── The Forge ──────────────────────────────────────────────────────────────
93
+ export function listForgeCampaigns(status) {
94
+ const qs = status ? `?status=${encodeURIComponent(status)}` : "";
95
+ const key = getApiKey();
96
+ if (key)
97
+ return request(`/api/v1/forge/campaigns${qs}`, { key });
98
+ return request(`/api/forge/campaigns${qs}`);
99
+ }
100
+ export function getForgeCampaign(campaignId) {
101
+ const id = encodeURIComponent(campaignId);
102
+ const key = getApiKey();
103
+ if (key)
104
+ return request(`/api/v1/forge/campaigns/${id}`, { key });
105
+ return request(`/api/forge/campaigns/${id}`);
106
+ }
107
+ // ─── Persona + account ──────────────────────────────────────────────────────
108
+ export function getPersona(playerId = "me") {
109
+ return request(`/api/v1/personas/${encodeURIComponent(playerId)}`, { key: requireKey() });
110
+ }
111
+ export function whoami() {
112
+ return request("/api/v1/me", { key: requireKey() });
113
+ }
114
+ // ─── Key management (Supabase account auth, not API-key auth) ───────────────
115
+ /** Sign in with a Snowbll email+password account; returns a Supabase access token. */
116
+ export async function supabaseLogin(email, password) {
117
+ const res = await fetch(`${getSupabaseUrl()}/auth/v1/token?grant_type=password`, {
118
+ method: "POST",
119
+ headers: { "content-type": "application/json", apikey: getSupabaseAnonKey() },
120
+ body: JSON.stringify({ email, password }),
121
+ });
122
+ const json = (await res.json().catch(() => ({})));
123
+ if (!res.ok || !json.access_token) {
124
+ throw new SnowbllApiError(res.status, json.error_description ||
125
+ json.msg ||
126
+ "Sign-in failed. Accounts created with Google/Discord have no password — ask the Snowbll team for a preview key instead.");
127
+ }
128
+ return json.access_token;
129
+ }
130
+ export function createKey(accessToken, name) {
131
+ return request("/api/v1/keys", { method: "POST", body: { name }, key: accessToken });
132
+ }
133
+ export function listKeys(accessToken) {
134
+ return request("/api/v1/keys", { key: accessToken });
135
+ }
136
+ export function revokeKey(accessToken, id) {
137
+ return request(`/api/v1/keys/${encodeURIComponent(id)}`, { method: "DELETE", key: accessToken });
138
+ }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { runStdioServer } from "../server.js";
4
+ import { runSetup } from "../setup.js";
5
+ const require = createRequire(import.meta.url);
6
+ const { version } = require("../../package.json");
7
+ /**
8
+ * `npx snowbll-mcp` — run the MCP server on stdio (what agents launch)
9
+ * `npx snowbll-mcp setup` — interactive setup wizard
10
+ */
11
+ const arg = process.argv[2];
12
+ if (arg === "setup") {
13
+ runSetup().catch((err) => {
14
+ console.error(err instanceof Error ? err.message : String(err));
15
+ process.exit(1);
16
+ });
17
+ }
18
+ else if (arg === "--version" || arg === "-v") {
19
+ console.log(version);
20
+ }
21
+ else if (arg && arg !== "serve") {
22
+ console.error(`Unknown command '${arg}'. Usage: snowbll-mcp [setup|serve|--version]`);
23
+ process.exit(1);
24
+ }
25
+ else {
26
+ runStdioServer(version).catch((err) => {
27
+ console.error(`[snowbll-mcp] fatal: ${err instanceof Error ? err.message : String(err)}`);
28
+ process.exit(1);
29
+ });
30
+ }