How to Build an AI‑Powered MCP Server to Control a Snake Game

This guide explains how to set up a Model Context Protocol (MCP) server, define its resources, tools, and prompt templates, implement both manual and WebSocket versions of a Snake game client, create MCP clients in TypeScript and Python, debug with the inspector, and integrate the server with AI agents for autonomous gameplay.

Alibaba Cloud Developer
Alibaba Cloud Developer
Alibaba Cloud Developer
How to Build an AI‑Powered MCP Server to Control a Snake Game

MCP accelerates AI development, allowing AI not only to speak but also to act. Manus has become popular, praised as "the next domestic star". OpenManus quickly joined forces with Manus, and Alibaba QwQ also collaborates actively. AI coding tools like Cursor and Cline have their own MCP Server Marketplace, forming a thriving AI‑tool ecosystem centered on MCP.

For individuals, AI and AI agents shift us from "what I can do" to "what else I can do", while AI also reflects ourselves.

This article introduces:

Current MCP Server ecosystem

How to implement an MCP Server

How to debug an MCP Server – inspector

How to achieve multi‑turn interaction – let AI play Snake

MCP Server Core Concepts

MCP Server provides three main capabilities:

Resources: Class file data that the client can read (e.g., API responses or file contents)

Tools: Functions that large language models can call (with user approval)

Prompt Templates: Pre‑crafted text templates that help users complete specific tasks

Note: MCP Server currently only supports local execution.

Official note: "Because servers are locally run, MCP currently only supports desktop hosts. Remote hosts are in active development."

The official documentation is brief, indicating that MCP Server focuses on practice. The following sections provide a hands‑on implementation.

MCP Server Example

Many MCP Server examples are available on the official GitHub:

https://github.com/modelcontextprotocol/servers

These include various JavaScript and Python implementations.

smithery.ai

mcpserver.org

pulsemcp.com

mcp.so

glama.ai/mcp/server

Practice – Implement Snake Game with MCP Server

First, a manual version of Snake is created. The AI writes the code instantly.

<!DOCTYPE html>
<html>
<head>
  <title>Snake Game</title>
  <style>
    canvas { border: 2px solid #333; background-color: #f0f0f0; }
    #score-panel { font-size: 24px; margin: 10px 0; }
  </style>
</head>
<body>
  <div id="score-panel">Score: 0</div>
  <canvas id="gameCanvas" width="400" height="400"></canvas>
  <script>
    // Game configuration and logic (omitted for brevity)
  </script>
</body>
</html>

After testing, a scoring panel is added.

WebSocket Version of Snake

The manual version is adapted to use a WebSocket channel, enabling both manual control and server communication.

Snake Game Client Implementation

The client connects to the MCP Server via WebSocket, handling direction, start, end, and state messages.

const socket = new WebSocket('ws://localhost:8080');
socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  // Process direction, start, end, get_state messages
};

Snake Game – Premium Version

A more polished UI with gradients, rounded borders, and glowing food is implemented.

<!DOCTYPE html>
<html>
<head>
  <title>Snake – Premium Edition</title>
  <style>
    canvas { border: 3px solid #2c3e50; border-radius: 10px; background: linear-gradient(145deg, #ecf0f1, #dfe6e9); }
    #score-panel { font-size: 24px; margin: 15px 0; color: #2c3e50; font-family: Arial, sans-serif; text-shadow: 1px 1px 2px rgba(0,0,0,0.1); }
    body { display: flex; flex-direction: column; align-items: center; background: #bdc3c7; min-height: 100vh; margin: 0; padding-top: 20px; }
  </style>
</head>
<body>
  <div id="score-panel">Score: 0</div>
  <canvas id="gameCanvas" width="400" height="400"></canvas>
  <script type="module">
    // WebSocket connection and enhanced drawing logic (omitted for brevity)
  </script>
</body>
</html>

MCP Client Implementation

The MCP Client connects to an MCP Server, lists available tools, and handles tool calls. Both TypeScript and Python versions are provided.

TypeScript MCP Client

/**
 * MCP client implementation
 * Provides connection, tool invocation, and chat interaction.
 */
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import OpenAI from "openai";
import * as dotenv from "dotenv";
import * as readline from "readline";

dotenv.config();

class MCPClient {
  private openai: OpenAI;
  private client: Client;
  private messages = [{ role: "system", content: "You are a versatile assistant capable of answering questions, completing tasks, and intelligently invoking specialized tools to deliver optimal results." }];
  private availableTools: any[] = [];

  constructor() {
    if (!process.env.OPENAI_API_KEY) throw new Error("OPENAI_API_KEY not set");
    this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, baseURL: process.env.OPENAI_BASE_URL });
    this.client = new Client({ name: "my-mcp-client", version: "1.0.0" });
  }

  async connectToServer(serverScriptPath: string) {
    const isPython = serverScriptPath.endsWith('.py');
    const isJs = serverScriptPath.endsWith('.js');
    if (!isPython && !isJs) throw new Error("Server script must be .py or .js");
    const command = isPython ? "python" : "node";
    const transport = new StdioClientTransport({ command, args: [serverScriptPath] });
    await this.client.connect(transport);
    const tools = (await this.client.listTools()).tools as any[];
    this.availableTools = tools.map(tool => ({
      type: "function",
      function: {
        name: tool.name,
        description: tool.description,
        parameters: { type: "object", properties: tool.inputSchema.properties, required: tool.inputSchema.required }
      }
    }));
    console.log("
Connected to server, tools:", tools.map(t => t.name));
  }

  private async toolCalls(response: any, messages: any[]) {
    let currentResponse = response;
    while (currentResponse.choices[0].message.tool_calls) {
      for (const toolCall of currentResponse.choices[0].message.tool_calls) {
        const toolName = toolCall.function.name;
        let toolArgs = {};
        try { toolArgs = JSON.parse(toolCall.function.arguments); } catch { console.error('Failed to parse args'); }
        const result = await this.client.callTool({ name: toolName, arguments: toolArgs });
        messages.push(currentResponse.choices[0].message);
        messages.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(result.content) });
      }
      currentResponse = await this.openai.chat.completions.create({ model: process.env.OPENAI_MODEL as string, messages, tools: this.availableTools });
    }
    return currentResponse;
  }

  async processQuery(query: string) {
    this.messages.push({ role: "user", content: query });
    let response = await this.openai.chat.completions.create({ model: process.env.OPENAI_MODEL as string, messages: this.messages, tools: this.availableTools });
    if (response.choices[0].message.tool_calls) {
      response = await this.toolCalls(response, this.messages);
    }
    this.messages.push(response.choices[0].message);
    return response.choices[0].message.content || "";
  }

  async chatLoop() {
    console.log("
MCP Client Started!
Type your queries or 'quit' to exit.");
    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
    while (true) {
      const query = await new Promise<string>(resolve => rl.question("
Query: ", resolve));
      if (query.toLowerCase() === 'quit') break;
      try {
        const response = await this.processQuery(query);
        console.log("
" + response);
      } catch (e) {
        console.error("
Error:", e instanceof Error ? e.message : String(e));
      }
    }
    rl.close();
  }

  async cleanup() { if (this.client) await this.client.close(); }
}

async function main() {
  if (process.argv.length < 3) { console.log("Usage: node dist/index.js <path_to_server_script>"); process.exit(1); }
  const client = new MCPClient();
  try {
    await client.connectToServer(process.argv[2]);
    await client.chatLoop();
  } finally { await client.cleanup(); }
}

main().catch(e => { console.error("Error:", e); process.exit(1); });

Python MCP Client

import asyncio, json, os, traceback
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()

class MCPClient:
    def __init__(self):
        self.session: Optional[ClientSession] = None
        self.exit_stack = AsyncExitStack()
        self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
        self.model = os.getenv("OPENAI_MODEL")
        self.messages = [{"role": "system", "content": "You are a versatile assistant capable of answering questions, completing tasks, and intelligently invoking specialized tools to deliver optimal results."}]
        self.available_tools = []

    @staticmethod
    def convert_custom_object(obj):
        if hasattr(obj, "__dict__"): return obj.__dict__
        if isinstance(obj, (list, tuple)): return [MCPClient.convert_custom_object(i) for i in obj]
        if isinstance(obj, dict): return {k: MCPClient.convert_custom_object(v) for k, v in obj.items()}
        return obj

    async def connect_to_server(self, server_script_path: str):
        is_python = server_script_path.endswith('.py')
        is_js = server_script_path.endswith('.js')
        if not (is_python or is_js): raise ValueError("Server script must be .py or .js")
        command = "python" if is_python else "node"
        server_params = StdioServerParameters(command=command, args=[server_script_path], env=None)
        stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
        self.stdio, self.write = stdio_transport
        self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
        await self.session.initialize()
        response = await self.session.list_tools()
        self.available_tools = [{"type": "function", "function": {"name": t.name, "description": t.description, "parameters": t.inputSchema}} for t in response.tools]
        print("
Connected to server with tools:", [t.name for t in response.tools])

    async def process_query(self, query: str) -> str:
        self.messages.append({"role": "user", "content": query})
        if not self.available_tools:
            resp = await self.session.list_tools()
            self.available_tools = [{"type": "function", "function": {"name": t.name, "description": t.description, "parameters": t.inputSchema}} for t in resp.tools]
        current = self.client.chat.completions.create(model=self.model, messages=self.messages, tools=self.available_tools)
        if current.choices[0].message.content:
            print("
🤖 AI:", current.choices[0].message.content)
        while current.choices[0].message.tool_calls:
            for tool_call in current.choices[0].message.tool_calls:
                name = tool_call.function.name
                try: args = json.loads(tool_call.function.arguments)
                except json.JSONDecodeError: args = {}
                print(f"
🔧 Calling tool {name}")
                result = await self.session.call_tool(name, args)
                self.messages.append(current.choices[0].message)
                self.messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result.content)})
            current = self.client.chat.completions.create(model=self.model, messages=self.messages, tools=self.available_tools)
        self.messages.append(current.choices[0].message)
        return current.choices[0].message.content or ""

    async def chat_loop(self):
        print("
MCP Client Started!
Type your queries or 'quit' to exit.")
        while True:
            try:
                query = input("
Command: ").strip()
                if query.lower() == 'quit': break
                response = await self.process_query(query)
                print("
🤖 AI:", response)
            except Exception as e:
                print(f"
Error: {e}")
                traceback.print_exc()

    async def cleanup(self):
        await self.exit_stack.aclose()

async def main():
    if len(sys.argv) < 2:
        print("Usage: python client.py <path_to_server_script>")
        sys.exit(1)
    client = MCPClient()
    try:
        await client.connect_to_server(sys.argv[1])
        await client.chat_loop()
    finally:
        await client.cleanup()

if __name__ == "__main__":
    import sys
    asyncio.run(main())

MCP Server Implementation (JavaScript)

#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { WebSocketServer } from 'ws';

class SnakeServer {
  constructor() {
    const WebSocket = require('ws');
    this.WebSocket = WebSocket;
    this.wss = new WebSocket.Server({ port: 8080 });
    this.wss.on('connection', ws => {
      console.log('client connected');
      ws.on('message', message => {
        try {
          const data = JSON.parse(message.toString());
          console.log(data);
          if (data.type === 'state') {
            this.gameState.snake = data.snake;
            this.gameState.food = data.food;
            this.gameState.score = data.score;
            this.gameState.direction = data.direction.dx > 0 ? 'right' : data.direction.dx < 0 ? 'left' : data.direction.dy > 0 ? 'down' : 'up';
            if (this.gameState.autoPathFind) {
              setTimeout(() => {
                const direction = this.calculateDirection(this.gameState);
                ws.send(JSON.stringify({ type: 'direction', direction, timestamp: Date.now() }));
              }, 100);
            }
          }
        } catch (err) { console.error('Failed to parse message:', err); }
      });
      ws.on('close', () => console.log('client closed'));
      ws.on('error', console.error);
    });
    this.server = new Server({ name: 'snake-server', version: '0.1.0' }, { capabilities: { tools: {} } });
    this.setupToolHandlers();
    this.server.onerror = error => console.error('[MCP Error]', error);
    process.on('SIGINT', async () => { await this.server.close(); process.exit(0); });
  }

  // Game state and helper methods omitted for brevity (moveHead, isCollision, calculateDirection, etc.)

  setupToolHandlers() {
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        { name: 'move_step', description: 'Move snake one step', inputSchema: { type: 'object', properties: { direction: { type: 'string', enum: ['up','down','left','right'] } }, required: ['direction'] } },
        { name: 'get_state', description: 'Get current game state', inputSchema: { type: 'object', properties: {} } },
        { name: 'auto_path_find', description: 'Enable auto‑move', inputSchema: { type: 'object', properties: {} } },
        { name: 'start_game', description: 'Start a new game', inputSchema: { type: 'object', properties: {} } },
        { name: 'end_game', description: 'End current game', inputSchema: { type: 'object', properties: {} } }
      ]
    }));

    this.server.setRequestHandler(CallToolRequestSchema, async request => {
      switch (request.params.name) {
        case 'move_step': {
          if (!request.params.arguments) throw new Error('Missing direction');
          const direction = request.params.arguments.direction;
          this.gameState.direction = direction;
          this.wss.clients.forEach(client => {
            if (client.readyState === this.WebSocket.OPEN) {
              client.send(JSON.stringify({ type: 'direction', direction, timestamp: Date.now() }));
            }
          });
          return { content: [{ type: 'text', text: `Direction updated, state: ${JSON.stringify(this.gameState, null, 2)}` }] };
        }
        case 'get_state': {
          return { content: [{ type: 'text', text: JSON.stringify(this.gameState, null, 2) }] };
        }
        case 'auto_path_find': {
          this.gameState.autoPathFind = true;
          this.wss.clients.forEach(client => {
            if (client.readyState === this.WebSocket.OPEN) {
              client.send(JSON.stringify({ type: 'get_state', timestamp: Date.now() }));
            }
          });
          return { content: [{ type: 'text', text: `Auto‑move enabled, state: ${JSON.stringify(this.gameState, null, 2)}` }] };
        }
        case 'start_game': {
          this.gameState.gameStarted = true;
          this.wss.clients.forEach(client => {
            if (client.readyState === this.WebSocket.OPEN) client.send(JSON.stringify({ type: 'start' }));
          });
          await new Promise(r => setTimeout(r, 100));
          return { content: [{ type: 'text', text: `Game started, state: ${JSON.stringify(this.gameState, null, 2)}` }] };
        }
        case 'end_game': {
          this.gameState.gameStarted = false;
          this.gameState.autoPathFind = false;
          this.wss.clients.forEach(client => {
            if (client.readyState === this.WebSocket.OPEN) client.send(JSON.stringify({ type: 'end' }));
          });
          return { content: [{ type: 'text', text: 'Game ended' }] };
        }
        default:
          return { content: [{ type: 'text', text: 'Unknown call' }] };
      }
    });
  }

  async run() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error('Snake MCP server started');
  }
}

const server = new SnakeServer();
server.run().catch(console.log);

MCP Debugging with Inspector

The inspector connects to a running MCP Server, lists its tools, and allows you to run any tool directly from the UI, showing input and output clearly.

Run the inspector with:

npx @modelcontextprotocol/inspector node ./build/index.js

Logs appear in the terminal, providing detailed information.

AI Playing Snake via MCP

Using the MCP Client, the AI is given the task "Play a game of Snake, auto‑move, monitor the score, and stop when the score exceeds 100". The AI selects appropriate tools (start_game, get_state, auto_path_find) and orchestrates the gameplay.

During execution, token consumption becomes a concern due to long multi‑turn conversations. Possible solutions include breaking the task into smaller subtasks, trimming context, or compressing messages.

Third‑Party Plugin Support for MCP Server

More platforms and plugins now support MCP Server, creating an ecosystem where a locally built MCP Server can be easily integrated. Cursor and Cline both have MCP Server marketplaces, allowing developers to add their servers with minimal configuration.

After installation, the server status is shown with green indicators for success and error logs for troubleshooting.

Running the server in Cline enables the AI to play Snake automatically.

Future ideas include having two AI agents play Chinese Chess via a shared MCP Server, illustrating the broader potential of AI‑agent collaboration.

Appendix – MCP Server Implementation

The full JavaScript source code for the Snake MCP Server is provided above.

AIMCPTool IntegrationserverSnake Game
Alibaba Cloud Developer
Written by

Alibaba Cloud Developer

Alibaba's official tech channel, featuring all of its technology innovations.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.