A PvP Battleship game backend with both REST API and MCP (Model Context Protocol) server for AI agents.
npm install-
Create a Supabase project
-
Run the database migration using one of these methods:
Option A: Supabase Dashboard (Easiest)
- Go to your project's SQL Editor
- Copy the contents of
supabase/migrations/001_create_tables.sql - Paste into the SQL Editor and click "Run"
Option B: Supabase CLI
# Install Supabase CLI if not already installed npm install -g supabase # Login to Supabase supabase login # Link to your project (find project ref in project settings) supabase link --project-ref your-project-ref # Push the migration supabase db push
Option C: Direct psql connection
# Find your connection string in Supabase Dashboard > Settings > Database psql "postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres" -f supabase/migrations/001_create_tables.sql
-
Get your API credentials from Supabase Dashboard > Settings > API:
SUPABASE_URL: Project URLSUPABASE_ANON_KEY: anon/public key
-
Copy
.env.exampleto.envand add your credentials:
cp .env.example .envPORT=3000
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your_supabase_anon_keynpm run build
npm start # REST API server
npm run mcp # MCP server- 10x10 grid per player
- Ships: Carrier (5), Battleship (4), Cruiser (3), Submarine (3), Destroyer (2)
- Players place ships, then take turns firing
- Game ends when all ships of one player are sunk
Base URL: http://localhost:3000
Interactive docs available at /api-docs when server is running.
GET /health
Response:
{ "status": "ok" }POST /games
Content-Type: application/json
{ "name": "Player1" }
Response:
{
"gameCode": "ABC123",
"gameId": "uuid",
"playerToken": "uuid",
"status": "waiting"
}POST /games/:code/join
Content-Type: application/json
{ "name": "Player2" }
Response:
{
"gameCode": "ABC123",
"gameId": "uuid",
"playerToken": "uuid",
"status": "placing"
}GET /games/:code
X-Player-Token: your-player-token
Response:
{
"game": {
"id": "uuid",
"code": "ABC123",
"status": "playing",
"current_turn": "player1",
"winner": null
},
"you": {
"name": "Player1",
"board": [...],
"shots": [...],
"ready": true
},
"opponent": {
"name": "Player2",
"shots": [...],
"ready": true,
"shipsSunk": 0
},
"isYourTurn": true
}POST /games/:code/place-ships
X-Player-Token: your-player-token
Content-Type: application/json
{
"ships": [
{
"name": "Carrier",
"size": 5,
"positions": [
{"x": 0, "y": 0},
{"x": 1, "y": 0},
{"x": 2, "y": 0},
{"x": 3, "y": 0},
{"x": 4, "y": 0}
]
},
{
"name": "Battleship",
"size": 4,
"positions": [
{"x": 0, "y": 1},
{"x": 1, "y": 1},
{"x": 2, "y": 1},
{"x": 3, "y": 1}
]
},
{
"name": "Cruiser",
"size": 3,
"positions": [
{"x": 0, "y": 2},
{"x": 1, "y": 2},
{"x": 2, "y": 2}
]
},
{
"name": "Submarine",
"size": 3,
"positions": [
{"x": 0, "y": 3},
{"x": 1, "y": 3},
{"x": 2, "y": 3}
]
},
{
"name": "Destroyer",
"size": 2,
"positions": [
{"x": 0, "y": 4},
{"x": 1, "y": 4}
]
}
]
}
Response:
{
"success": true,
"message": "Ships placed successfully"
}POST /games/:code/fire
X-Player-Token: your-player-token
Content-Type: application/json
{ "x": 5, "y": 3 }
Response:
{
"hit": true,
"sunk": "Destroyer",
"gameOver": false,
"winner": null
}Clients can subscribe to game updates in real-time using Supabase Realtime. This is useful for:
- Knowing when an opponent joins the game
- Knowing when an opponent places their ships
- Getting notified when it's your turn
- Receiving shot results without polling
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// Subscribe to game updates
const gameSubscription = supabase
.channel('game-updates')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'games',
filter: `code=eq.${gameCode}`
},
(payload) => {
console.log('Game updated:', payload.new);
// Handle status changes, turn changes, winner
}
)
.subscribe();
// Subscribe to player updates (shots fired, ships placed)
const playerSubscription = supabase
.channel('player-updates')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'players',
filter: `game_id=eq.${gameId}`
},
(payload) => {
console.log('Player updated:', payload.new);
// Handle opponent ready status, new shots
}
)
.subscribe();
// Cleanup when done
gameSubscription.unsubscribe();
playerSubscription.unsubscribe();| Table | Event | Use Case |
|---|---|---|
games |
UPDATE | Status changes (waiting→placing→playing→finished), turn changes, winner |
players |
UPDATE | Opponent ready (ships placed), new shots fired |
players |
INSERT | Opponent joined the game |
The MCP server exposes the Battleship API as tools for AI coding agents. It supports two transport modes:
- stdio - For local CLI usage
- HTTP/SSE - For remote access over the network
| Tool | Description | Parameters |
|---|---|---|
create_game |
Create a new game | name |
join_game |
Join existing game | code, name |
get_game_state |
Get current state | playerToken |
place_ships |
Place ships on board | playerToken, ships |
fire |
Fire at coordinate | playerToken, x, y |
get_realtime_instructions |
Get code for live subscriptions | gameId?, gameCode? |
Local (stdio mode):
npm run mcpHTTP mode (for remote access):
npm run mcp:http
# Server runs on port 3001 (or PORT/MCP_PORT env var)When running in HTTP mode, the server exposes:
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check |
/mcp |
POST | Client-to-server messages (JSON-RPC) |
/mcp |
GET | Server-to-client SSE stream |
/mcp |
DELETE | Terminate session |
The /mcp endpoint uses the MCP Streamable HTTP transport (protocol version 2025-11-25) with session management via MCP-Session-Id header.
Option 1: Local stdio (Claude Code settings)
Add to ~/.claude/settings.json:
{
"mcpServers": {
"battleship": {
"command": "node",
"args": ["/path/to/battleship-api/dist/mcp-server.js"],
"env": {
"SUPABASE_URL": "your_supabase_url",
"SUPABASE_ANON_KEY": "your_supabase_anon_key"
}
}
}
}Option 2: Remote Streamable HTTP
For MCP clients that support Streamable HTTP transport, connect to:
MCP URL: https://your-mcp-server.run.app/mcp
Option 3: Project-specific config
Add to .claude/settings.local.json:
{
"mcpServers": {
"battleship": {
"command": "npm",
"args": ["run", "mcp"],
"cwd": "/path/to/battleship-api",
"env": {
"SUPABASE_URL": "your_supabase_url",
"SUPABASE_ANON_KEY": "your_supabase_anon_key"
}
}
}
}Option 4: Codex CLI (config.toml)
Run the HTTP server and add to ~/.codex/config.toml:
[mcp_servers.battleship]
url = "http://localhost:3001/mcp"Option 5: GitHub Copilot (VS Code)
- Run the HTTP server (
npm run mcp:http) - In VS Code, open Settings and search for "MCP" under GitHub Copilot
- Add a new MCP server named
battleshipwith URLhttp://localhost:3001/mcp
Agent 1 (creates game):
create_game(name: "AI-Player-1")
→ { gameCode: "XYZ789", playerToken: "token1" }
Agent 2 (joins game):
join_game(code: "XYZ789", name: "AI-Player-2")
→ { playerToken: "token2" }
Both agents place ships:
place_ships(playerToken: "token1", ships: [...])
place_ships(playerToken: "token2", ships: [...])
Take turns firing:
fire(playerToken: "token1", x: 5, y: 3)
→ { hit: true, sunk: null, gameOver: false }
fire(playerToken: "token2", x: 0, y: 0)
→ { hit: false, sunk: null, gameOver: false }
| Script | Description |
|---|---|
npm run dev |
Development server with hot reload |
npm run build |
Compile TypeScript |
npm start |
Run REST API server |
npm run mcp |
Run MCP server (stdio) |
npm run mcp:http |
Run MCP server (HTTP/SSE) |
Both the REST API and MCP HTTP server can be deployed to Google Cloud Run using the included GitHub Actions workflow.
The workflow deploys two services:
battleship-api- REST API serverbattleship-mcp- MCP HTTP/SSE server
-
A Google Cloud project with billing enabled
-
Enable required APIs:
gcloud services enable \ run.googleapis.com \ artifactregistry.googleapis.com \ secretmanager.googleapis.com \ iamcredentials.googleapis.com -
Create an Artifact Registry repository:
gcloud artifacts repositories create battleship-api \ --repository-format=docker \ --location=europe-north2
-
Store Supabase secrets in Secret Manager:
echo -n "your-supabase-url" | gcloud secrets create SUPABASE_URL --data-file=- echo -n "your-supabase-anon-key" | gcloud secrets create SUPABASE_ANON_KEY --data-file=-
-
Grant Cloud Run access to secrets (uses default Compute Engine service account):
PROJECT_NUMBER=$(gcloud projects describe $(gcloud config get-value project) --format='value(projectNumber)') gcloud secrets add-iam-policy-binding SUPABASE_URL \ --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \ --role="roles/secretmanager.secretAccessor" gcloud secrets add-iam-policy-binding SUPABASE_ANON_KEY \ --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \ --role="roles/secretmanager.secretAccessor"
-
Create a Workload Identity Pool:
gcloud iam workload-identity-pools create "github-pool" \ --location="global" \ --display-name="GitHub Actions Pool"
-
Create a Workload Identity Provider (replace
YOUR_GITHUB_USERNAME/battleship-apiwith your repo):gcloud iam workload-identity-pools providers create-oidc "github-provider" \ --location="global" \ --workload-identity-pool="github-pool" \ --display-name="GitHub Provider" \ --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \ --attribute-condition="assertion.repository=='YOUR_GITHUB_USERNAME/battleship-api'" \ --issuer-uri="https://token.actions.githubusercontent.com"
-
Create a Service Account and grant permissions:
gcloud iam service-accounts create github-actions-sa \ --display-name="GitHub Actions Service Account" # Grant necessary roles PROJECT_ID=$(gcloud config get-value project) SA_EMAIL="github-actions-sa@${PROJECT_ID}.iam.gserviceaccount.com" gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:${SA_EMAIL}" \ --role="roles/run.admin" gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:${SA_EMAIL}" \ --role="roles/artifactregistry.writer" gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:${SA_EMAIL}" \ --role="roles/secretmanager.secretAccessor" gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:${SA_EMAIL}" \ --role="roles/iam.serviceAccountUser"
-
Allow GitHub Actions to impersonate the Service Account:
PROJECT_ID=$(gcloud config get-value project) REPO="your-github-username/battleship-api" gcloud iam service-accounts add-iam-policy-binding \ "github-actions-sa@${PROJECT_ID}.iam.gserviceaccount.com" \ --role="roles/iam.workloadIdentityUser" \ --member="principalSet://iam.googleapis.com/projects/$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')/locations/global/workloadIdentityPools/github-pool/attribute.repository/${REPO}"
Add these to your GitHub repository:
Variables (Settings → Secrets and variables → Actions → Variables):
GCP_PROJECT_ID: Your Google Cloud project IDGCP_REGION: Deployment region (default:europe-north2)
Secrets (Settings → Secrets and variables → Actions → Secrets):
WIF_PROVIDER:projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github-providerWIF_SERVICE_ACCOUNT:github-actions-sa@PROJECT_ID.iam.gserviceaccount.com
Push to main branch to trigger automatic deployment, or manually trigger via GitHub Actions.
ISC