Skip to main content
Glama
code-cheers

魂斗罗 MCP Demo

by code-cheers
README.md
# 魂斗罗 MCP Demo

This project follows the architecture from Chris Nager's DOOM MCP App write-up, but uses an original placeholder browser game with Contra-style horizontal shooting.

The important part is not "MCP creates a game." The point is:

> MCP can return a live app UI, not just text or JSON. In compatible clients, the game appears inside the conversation itself.

No original Contra assets, music, characters, levels, or ROM data are included.

## Main Flow

- `create_game_session`: creates an inline game session and returns MCP App metadata so compatible hosts can render the game inline. It intentionally does not expose the browser fallback URL.
- `get_game_launch_url`: creates a signed `/game/play?token=...` URL for hosts that cannot render MCP Apps inline.
- `/game/play?token=...`: browser fallback route. The token is enough to boot the same game session without durable server-side session storage.
- `ui://contra-style/mcp-app.html`: the inline MCP App resource. It listens for the `create_game_session` tool result, extracts the session, and runs the game directly in the host iframe.

Optional extension tools:

- `update-game-state`: the MCP App can publish score, HP, position, enemies, ammo, and stage.
- `get-game-state`: reads the latest state.
- `suggest-next-move`: returns a Chinese gameplay suggestion from the latest state.

## Run

```bash
npm install
npm run build
npm run serve
```

MCP endpoint:

```text
http://localhost:3001/mcp
```

## Use From Codex Or Another MCP Host

Ask the host to call:

```text
create_game_session
```

If the host supports MCP Apps, it should render the game inline from:

```text
ui://contra-style/mcp-app.html
```

That is the primary demo: the host calls a tool, and the tool result causes a playable game UI to appear in the chat surface. Do not show or open a fallback link in this path.

If inline rendering is unavailable, call:

```text
get_game_launch_url
```

Then open the returned `/game/play?token=...` URL.

## Use From Claude Desktop

Claude Desktop renders MCP App UI resources inline in the chat (the client advertises the `io.modelcontextprotocol/ui` capability with `text/html;profile=mcp-app`). The matching local config:

`~/Library/Application Support/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "contra-demo": {
      "command": "npx",
      "args": ["-y", "mcp-remote@latest", "http://localhost:3001/mcp", "--allow-http"]
    }
  }
}
```

The `mcp-remote` bridge runs as a stdio child process inside Claude Desktop and forwards JSON-RPC to the local HTTP endpoint. The `--allow-http` flag is required because `mcp-remote` defaults to refusing non-HTTPS URLs. Quit and reopen Claude Desktop after editing the config, then ask Claude to call `create_game_session` — the playable game appears as a widget in the conversation.

> **Note:** the *Custom Connectors* UI on `claude.ai` (web) is **Remote MCP** — those requests originate from Anthropic's servers and cannot reach `localhost`. Use Claude Desktop with the file-based config above for a local URL, or expose the server over a public tunnel for the web Connector route.

### Run as a background service (macOS LaunchAgent)

If you start the server from an interactive shell or a Claude Code session, it dies when you quit that process. To keep it always-on, register it with `launchd`:

`~/Library/LaunchAgents/com.contra-demo.mcp.plist`

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>             <string>com.contra-demo.mcp</string>
    <key>ProgramArguments</key>  <array>
        <string>/PATH/TO/npm</string>
        <string>run</string>
        <string>serve</string>
    </array>
    <key>WorkingDirectory</key>  <string>/PATH/TO/deep-mcp</string>
    <key>EnvironmentVariables</key><dict>
        <key>PATH</key>          <string>/PATH/TO/node/bin:/usr/bin:/bin</string>
    </dict>
    <key>RunAtLoad</key>         <true/>
    <key>KeepAlive</key>         <true/>
    <key>StandardOutPath</key>   <string>/tmp/contra-demo-mcp.out.log</string>
    <key>StandardErrorPath</key> <string>/tmp/contra-demo-mcp.err.log</string>
</dict>
</plist>
```

```bash
launchctl load -w ~/Library/LaunchAgents/com.contra-demo.mcp.plist     # start + auto-start at login
launchctl list | grep contra-demo                                       # check
launchctl unload -w ~/Library/LaunchAgents/com.contra-demo.mcp.plist   # stop
```

`launchd` is independent of any GUI app, so quitting Claude Desktop / Terminal / IDE no longer kills the server. `KeepAlive=true` also restarts it if it crashes.

## Browser Fallback

After `npm run build && npm run serve`, call `get_game_launch_url` and open the returned URL.

For quick UI development only:

```bash
npm run dev
```

That opens the game without a signed session, so it is useful for visual testing but not the main demo flow.

## Controls

- Move: `WASD` or arrow keys
- Shoot: `Space` or `J`
- Restart: `R`

## Video Talking Points

- This is modeled after the DOOM MCP App architecture, not a from-scratch game-dev tutorial.
- The headline is: MCP can return an interactive game UI inside the conversation.
- The primary path is inline-only; the signed browser launch URL is exposed only by `get_game_launch_url`.
- The MCP App runs the game directly in the host iframe; it does not iframe another page inside the app.
- The real MCP lesson: protocol + UI resource + signed launch flow can turn a web capability into an AI-client-native app.

## Verification

```bash
npm run check
npm run build
curl -sS -X POST http://localhost:3001/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```

Maintenance

ActivityInactive
ResponsivenessNo issues