cartridge-cafe-mcp 0.1.0

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.
Files changed (3) hide show
  1. package/README.md +38 -0
  2. package/index.mjs +153 -0
  3. package/package.json +27 -0
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # cartridge-cafe-mcp
2
+
3
+ Your AI walks into [cartridge.cafe](https://cartridge.cafe) and builds live GPU worlds.
4
+
5
+ Worlds are text: WGSL shaders for the look, one JS step-hook for the rules. This
6
+ server gives any MCP client the whole loop — browse the shelf, read any public
7
+ world's full source, brew a world of your own through the **guest door** (no
8
+ account; three creations on the house, unlimited editing), and build it over the
9
+ bridge. Sign in on the site later and everything your AI made transfers to you.
10
+
11
+ ## Install (Claude Code)
12
+
13
+ ```bash
14
+ claude mcp add cartridge-cafe -- npx -y cartridge-cafe-mcp
15
+ ```
16
+
17
+ Or in any MCP client config:
18
+
19
+ ```json
20
+ { "mcpServers": { "cartridge-cafe": { "command": "npx", "args": ["-y", "cartridge-cafe-mcp"] } } }
21
+ ```
22
+
23
+ ## Tools
24
+
25
+ | tool | what it does |
26
+ |---|---|
27
+ | `read_guide` | the engine guide — mandatory before building |
28
+ | `browse_shelf` | every world, with play URLs |
29
+ | `read_world_source` | any public world's complete source (the shelf is a library) |
30
+ | `brew_world` | your own world via the guest door — returns a build token |
31
+ | `bridge` | send any engine command (fields, WGSL visuals, hooks, world data) |
32
+ | `world_state` | read a world's current state |
33
+ | `my_worlds` | what you've brewed this session + how to claim it |
34
+
35
+ Then tell your AI: *"brew me a world where…"*
36
+
37
+ The tournament — not edit access — decides what becomes canon. Original worlds
38
+ are immortal.
package/index.mjs ADDED
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ // cartridge-cafe-mcp — the cafe's door, installed inside your AI's house.
3
+ //
4
+ // Tools for browsing the shelf, reading any world's source, brewing a world
5
+ // through the GUEST door (no account — three creations on the house), and
6
+ // building it over the bridge. Everything speaks to the live site.
7
+
8
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
10
+ import { z } from 'zod'
11
+
12
+ const BASE = process.env.CAFE_BASE || 'https://cartridge.cafe'
13
+
14
+ // ── one guest session per server run: cookie jar + the worlds we brewed ──
15
+ const jar = {}
16
+ const cookies = () => Object.entries(jar).map(([k, v]) => `${k}=${v}`).join('; ')
17
+ const sip = (res) => {
18
+ for (const c of res.headers.getSetCookie?.() || []) {
19
+ const [kv] = c.split(';')
20
+ const i = kv.indexOf('=')
21
+ jar[kv.slice(0, i)] = kv.slice(i + 1)
22
+ }
23
+ }
24
+ const mine = [] // { name, slug, token, viewUrl }
25
+
26
+ const H = (extra = {}) => ({ 'Content-Type': 'application/json', Origin: BASE, cookie: cookies(), ...extra })
27
+ const jfetch = async (path, opts = {}) => {
28
+ const res = await fetch(BASE + path, { ...opts, headers: { ...H(), ...(opts.headers || {}) } })
29
+ sip(res)
30
+ const text = await res.text()
31
+ let body
32
+ try { body = JSON.parse(text) } catch { body = text }
33
+ return { status: res.status, body }
34
+ }
35
+
36
+ async function ensureGuest() {
37
+ const s = await jfetch('/api/auth/session')
38
+ if (s.body?.user) return true
39
+ await jfetch('/api/auth/guest', { method: 'POST' })
40
+ const csrf = (await jfetch('/api/auth/csrf')).body?.csrfToken
41
+ await jfetch('/api/auth/callback/guest', {
42
+ method: 'POST', redirect: 'manual',
43
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
44
+ body: new URLSearchParams({ csrfToken: csrf, json: 'true' }),
45
+ })
46
+ const s2 = await jfetch('/api/auth/session')
47
+ return !!s2.body?.user
48
+ }
49
+
50
+ const text = (s) => ({ content: [{ type: 'text', text: typeof s === 'string' ? s : JSON.stringify(s, null, 2) }] })
51
+
52
+ const server = new McpServer({ name: 'cartridge-cafe', version: '0.1.0' })
53
+
54
+ server.tool(
55
+ 'read_guide',
56
+ 'The engine guide — MANDATORY reading before building. Contracts for visuals (WGSL), step hooks (JS), fields, and every bridge command.',
57
+ {},
58
+ async () => {
59
+ const r = await fetch(BASE + '/api/engine/guide')
60
+ return text(await r.text())
61
+ },
62
+ )
63
+
64
+ server.tool(
65
+ 'browse_shelf',
66
+ "Every world on the cafe's shelf, with play URLs. Public worlds' full source is readable via read_world_source.",
67
+ {},
68
+ async () => {
69
+ const r = await jfetch('/api/engine/scene?action=list')
70
+ const scenes = r.body?.scenes || []
71
+ const sp = await jfetch('/api/spaces/browse')
72
+ const spaces = (sp.body?.spaces || []).map(s => ({ name: s.name || s.slug, play: `${BASE}/space/${s.slug}` }))
73
+ return text({
74
+ worlds: scenes.map(n => ({ name: n, play: `${BASE}/play/${encodeURIComponent(n)}` })),
75
+ playerWorlds: spaces,
76
+ note: 'Branches are named "BASE ⑂ handle · vN". Fork anything; a tournament decides canon.',
77
+ })
78
+ },
79
+ )
80
+
81
+ server.tool(
82
+ 'read_world_source',
83
+ "A public world's complete source — WGSL visuals, step-hook code, fields, params. The shelf is a library, not a vault: learn techniques from working worlds.",
84
+ { name: z.string().describe('World name exactly as it appears on the shelf') },
85
+ async ({ name }) => {
86
+ const r = await jfetch('/api/engine/library?world=' + encodeURIComponent(name))
87
+ return text(r.body)
88
+ },
89
+ )
90
+
91
+ server.tool(
92
+ 'brew_world',
93
+ 'Create YOUR OWN world through the guest door — no account needed. Returns a build token (uc_st_) for the bridge. Guests get three creations; editing is unlimited. Sign in on the site later and everything transfers to your account.',
94
+ { name: z.string().describe('The world\'s name') },
95
+ async ({ name }) => {
96
+ if (!(await ensureGuest())) return text({ error: 'could not open a guest session' })
97
+ const w = await jfetch('/api/spaces', { method: 'POST', body: JSON.stringify({ name }) })
98
+ if (!w.body?.space) return text({ error: w.body?.error || `create failed (${w.status})` })
99
+ const slug = w.body.space.slug
100
+ const t = await jfetch(`/api/spaces/${slug}/token`, { method: 'POST', body: JSON.stringify({ name: 'mcp' }) })
101
+ if (!t.body?.token) return text({ error: t.body?.error || 'token mint failed' })
102
+ const world = { name, slug, token: t.body.token, viewUrl: `${BASE}/space/${slug}` }
103
+ mine.push(world)
104
+ return text({
105
+ ...world,
106
+ next: 'Read the guide (read_guide), then build with the bridge tool. EVERY field needs a visualType or it renders as nothing. Ship worldData.instructions before you call it done.',
107
+ })
108
+ },
109
+ )
110
+
111
+ server.tool(
112
+ 'bridge',
113
+ 'Send a command (or {"commands":[...]} batch) to a world over the bridge — create_field, define_visual, add_step_hook, set_world_data, and the rest per the guide. Uses your most recently brewed world unless a token is given (also accepts uc_sc_ branch tokens from connect prompts).',
114
+ {
115
+ command: z.record(z.any()).describe('The bridge command object'),
116
+ token: z.string().optional().describe('World token (uc_st_/uc_sc_). Defaults to your latest brewed world.'),
117
+ },
118
+ async ({ command, token }) => {
119
+ const tok = token || mine[mine.length - 1]?.token
120
+ if (!tok) return text({ error: 'no world token — brew_world first, or pass one' })
121
+ const r = await fetch(BASE + '/api/engine/bridge', {
122
+ method: 'POST',
123
+ headers: { 'Content-Type': 'application/json', Origin: BASE, Authorization: `Bearer ${tok}` },
124
+ body: JSON.stringify(command),
125
+ })
126
+ return text(await r.json().catch(() => ({ status: r.status })))
127
+ },
128
+ )
129
+
130
+ server.tool(
131
+ 'world_state',
132
+ 'Read a world\'s current state over the bridge — fields, visuals, hooks, worldData. Defaults to your latest brewed world.',
133
+ { token: z.string().optional() },
134
+ async ({ token }) => {
135
+ const tok = token || mine[mine.length - 1]?.token
136
+ if (!tok) return text({ error: 'no world token — brew_world first, or pass one' })
137
+ const r = await fetch(BASE + '/api/engine/bridge', { headers: { Authorization: `Bearer ${tok}` } })
138
+ return text(await r.json().catch(() => ({ status: r.status })))
139
+ },
140
+ )
141
+
142
+ server.tool(
143
+ 'my_worlds',
144
+ 'The worlds you have brewed in this session, with their tokens and view URLs.',
145
+ {},
146
+ async () => text({
147
+ worlds: mine,
148
+ claim: 'These live under a guest deed. Sign in at ' + BASE + ' in a browser holding this machine\'s cookies and they transfer to the account permanently.',
149
+ }),
150
+ )
151
+
152
+ const transport = new StdioServerTransport()
153
+ await server.connect(transport)
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "cartridge-cafe-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for cartridge.cafe — your AI walks into the cafe and builds live GPU worlds. Guest door included: no account needed for the first three creations.",
5
+ "type": "module",
6
+ "bin": { "cartridge-cafe-mcp": "./index.mjs" },
7
+ "files": ["index.mjs", "README.md"],
8
+ "keywords": [
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "cartridge-cafe",
12
+ "webgpu",
13
+ "game-dev",
14
+ "ai-agents",
15
+ "wgsl",
16
+ "claude"
17
+ ],
18
+ "homepage": "https://cartridge.cafe",
19
+ "repository": { "type": "git", "url": "https://github.com/GalenGoodwick/Cartridge-Cafe.git", "directory": "mcp" },
20
+ "bugs": { "url": "https://github.com/GalenGoodwick/Cartridge-Cafe/issues" },
21
+ "license": "MIT",
22
+ "engines": { "node": ">=18" },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.0.0",
25
+ "zod": "^3.23.0"
26
+ }
27
+ }