Skip to main content

The Moonspell MCP server

https://mcp.moonspell.app/mcp is a remote MCP server for a developer's own coding agent (Claude Code, Claude.ai, Cursor, Codex or any MCP client). It is the front door of local developer mode: the person signs in with Google, and the agent gets the engine's skills and docs, the person's games, share links with a QR code, invites, and the commands that set a game folder up on their computer. How to add it to an agent, and how it differs from the public docs server, is on MCP servers.

A remote server cannot touch the developer's files, so everything that needs the folder (save, publish, sync the engine, continue a game, link a folder) stays with the crescent CLI and its local stdio server (crescent mcp). This server tells the agent which command to run, and hands the CLI a one-time sign-in code so the person does not sign in twice.

The code is in mcp/: its own Cloudflare Worker, moonspell-mcp, with its own package.json. The one route it needs on the Moonspell server is POST /api/internal/dev-session (src/app/api/internal/dev-session/).

What it offers​

Instructions, sent at initialize: what Moonspell is, that a game is a folder made from the starter with the engine's skills, which skills to load first (game-design-doc, then game-router for a new game), to save after every change, that publishing is public at once, and that local work goes through the crescent CLI.

Tools. Where a tool has a twin in crescent mcp, it has the same name; the remote ones take a gameId where the local ones read the game from the folder.

ToolWhat it does
moonspell_statusWho is signed in through this connection, and which starter the skills and docs come from
moonspell_gamesThe games this account owns or collaborates on, with the latest share link and the developer page
moonspell_game {gameId}One game: its versions, the people building it, its developer page
moonspell_create_game {title}Registers a new game (no folder yet; moonspell_start makes it)
moonspell_links {gameId}The latest version's share link, its QR code as text and as a PNG image, and the developer page
moonspell_invite {gameId, kind, maxUses?, expiresInDays?}An invite link: play or build (owner only)
moonspell_start {gameId?, dir?, title?}The commands that set up a folder on this computer, signed in with a one-time code
moonspell_skillsEvery engine skill with what it is for
moonspell_skill {name}One skill's SKILL.md and the paths of its other files
moonspell_skill_file {name, path}One of those files (references, recipes, the backend scaffold)
moonspell_engine_docs {topic?}The template's docs/ and its rules; without a topic, the list

Resources: moonspell://skills/<name> and moonspell://docs/<topic>. The skills are also served through the MCP Skills extension as skill://moonspell/<skill>/SKILL.md, so a client that supports it loads them as skills without tool calls.

Prompts: new_game {idea} and change_game {request}: load the right skills first, build the smallest playable version, save after every change, tell the developer before publishing.

Where the skills come from. mcp/scripts/bundle.mts runs before every wrangler dev and wrangler deploy (the build command in mcp/wrangler.jsonc). It reads the template's tracked .claude/skills/, docs/ and CLAUDE.md and lays templates/crescent-game-local/ over them the way scripts/pack-starter.mts does, so the server serves the starter's wording. A change to the rewording in scripts/pack-starter.mts needs the same change in bundle.mts. Every skill and doc answer ends with the starter version it came from. Inside a game folder, the folder's own .claude/skills win.

How signing in works​

One Worker plays both OAuth roles, the authorization server for MCP clients and the MCP resource they call, with @cloudflare/workers-oauth-provider's OAuthProvider. The provider serves the discovery documents (/.well-known/oauth-authorization-server, /.well-known/oauth-protected-resource/mcp), the token endpoint, dynamic client registration and Client ID Metadata Documents. The Worker's own code (mcp/src/auth.ts) is the consent page and the Google step:

  1. GET /authorize validates the client, its exact redirect URI, the resource and PKCE, then shows a consent page: the app's name (marked as not verified for a self-registered client), where its access goes, and what it will be able to do.
  2. POST /authorize records the approval and redirects to Google, with its own PKCE and a nonce.
  3. GET /callback exchanges Google's code for an ID token and checks it. The Worker sends the ID token to POST /api/internal/dev-session on the Moonspell server, which verifies it again, finds or makes the registered account, and mints a dev token named MCP: <client name>. The dev token is stored only in the grant's encrypted properties, and the client gets its code.

On every MCP request, the grant gives the dev token, which is the bearer of every /api/dev/* call made for that person. Access tokens last an hour; a grant lives while the client keeps refreshing it and ends after 30 idle days.

Revoking the token in Settings ends the connection. On a refresh the Worker checks the dev token, and a revoked one ends the grant; a tool call that gets a 401 ends the grant at once and says to reconnect.

Security properties the end-to-end test checks:

  • Consent is asked per client before any redirect to Google, so one client cannot ride on another's approval. A remembered approval is opt-in, lasts 30 days and is bound to the client, its redirect URI and the resource.
  • The consent page cannot be framed, works once, and is bound to the browser; the approved request comes from storage, never from the form.
  • PKCE is S256 only. Redirect URIs must match exactly, and errors are redirected only once the client and its redirect URI are valid.
  • The dev token never appears in storage in the clear, in a URL, in a log line or in a tool's answer.
  • The Google ID token is verified by both the Worker and the Moonspell server, so the server never takes the Worker's word for who someone is.
  • The Worker has its own host name, so its OAuth endpoints never pass through the edge Worker.

The main libraries are agents (createMcpHandler, a stateless MCP handler with no Durable Object), the MCP TypeScript SDK and @cloudflare/workers-oauth-provider.

Local development and tests​

cd mcp && npm install
npm test # bundle, unit tests, then the end-to-end test under wrangler dev --local
npm run typecheck # bundle, then tsc
npm run dev # wrangler dev --local on http://127.0.0.1:3231 (needs mcp/.dev.vars)

The end-to-end test (mcp/test/e2e.test.ts) starts the Worker and an in-memory stub of Google and of the Moonspell server (mcp/test/fakes.ts): no network, no real account, no database. It registers a client, signs in through the consent page, exchanges the code with PKCE, connects the MCP SDK's client, lists tools, resources and prompts, reads skills, creates a game, gets the hand-off commands, decodes the QR code, makes an invite, then checks the failure cases: a wrong PKCE verifier, a forged consent post, an unregistered redirect URI, denying, a replayed callback, an unverified email, refresh and revocation.

For npm run dev against a real Google OAuth client, copy mcp/.dev.vars.example to mcp/.dev.vars and add http://127.0.0.1:3231/callback to that client's redirect URIs.