Skip to main content
Glama
server.ts22 kB
import { Server as SocketBE, ServerEvent, World, Agent } from "socket-be"; import { v4 as uuidv4 } from "uuid"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { ConnectedPlayer, ToolCallResult, } from "./types"; // Advanced Building ツール import { BuildCubeTool } from "./tools/advanced/building/build-cube"; import { BuildLineTool } from "./tools/advanced/building/build-line"; import { BuildSphereTool } from "./tools/advanced/building/build-sphere"; import { BuildParaboloidTool } from "./tools/advanced/building/build-paraboloid"; import { BuildHyperboloidTool } from "./tools/advanced/building/build-hyperboloid"; import { BuildCylinderTool } from "./tools/advanced/building/build-cylinder"; import { BuildTorusTool } from "./tools/advanced/building/build-torus"; import { BuildHelixTool } from "./tools/advanced/building/build-helix"; import { BuildEllipsoidTool } from "./tools/advanced/building/build-ellipsoid"; import { BuildRotateTool } from "./tools/advanced/building/build-rotate"; import { BuildTransformTool } from "./tools/advanced/building/build-transform"; import { BuildBezierTool } from "./tools/advanced/building/build-bezier"; // Socket-BE Core API ツール(推奨) import { AgentTool } from "./tools/core/agent"; import { WorldTool } from "./tools/core/world"; import { PlayerTool } from "./tools/core/player"; import { BlocksTool } from "./tools/core/blocks"; import { SystemTool } from "./tools/core/system"; import { CameraTool } from "./tools/core/camera"; import { SequenceTool } from "./tools/core/sequence"; import { MinecraftWikiTool } from "./tools/core/minecraft-wiki"; import { BaseTool } from "./tools/base/tool"; import { initializeLocale, SupportedLocale } from "./utils/i18n/locale-manager"; import { optimizeBuildResult, optimizeCommandResult, checkResponseSize, } from "./utils/token-optimizer"; import { SchemaToZodConverter } from "./utils/schema-converter"; import { enrichErrorWithHints } from "./utils/error-hints"; /** * Minecraft Bedrock Edition用MCPサーバー * * WebSocket接続を通じてMinecraft Bedrock Editionを制御し、 * MCP(Model Context Protocol)プロトコルを実装して * AIクライアント(Claude Desktopなど)との統合を提供します。 * * @description * このサーバーは以下の機能を提供します: * - WebSocket経由でのMinecraft Bedrock Edition接続 * - MCP 2.0プロトコル準拠のAIクライアント統合 * - 15種類の階層化ツール(基本操作・複合操作) * - プレイヤー、エージェント、ワールド、建築制御 * * @example * ```typescript * // サーバーの起動 * const server = new MinecraftMCPServer(); * server.start(8001); * * // Minecraftから接続: /connect localhost:8001/ws * ``` * * @since 1.0.0 * @author mcbk-mcp contributors * @see {@link https://github.com/Mming-Lab/minecraft-bedrock-mcp-server} * @see {@link https://modelcontextprotocol.io/} MCP Protocol */ export class MinecraftMCPServer { private connectedPlayer: ConnectedPlayer | null = null; private socketBE: SocketBE | null = null; private tools: BaseTool[] = []; private currentWorld: World | null = null; private currentAgent: Agent | null = null; private mcpServer: McpServer; constructor() { // MCP公式SDKのサーバーを初期化 this.mcpServer = new McpServer( { name: "minecraft-bedrock-education-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); } /** * MCPサーバーを起動します * * WebSocketサーバーとMCPインターフェースを初期化し、 * Minecraftクライアントとの接続を待機します。 * * @param port - WebSocketサーバーのポート番号(デフォルト: 8001) * @throws WebSocketサーバーの起動に失敗した場合 * * @example * ```typescript * const server = new MinecraftMCPServer(); * server.start(8001); // ポート8001で起動 * * // Minecraftから接続: * // /connect localhost:8001/ws * ``` */ public async start(port: number = 8001, locale?: SupportedLocale): Promise<void> { // 言語設定を初期化 initializeLocale(locale); // MCP及びツールの初期化 await this.setupMCPServer(); // Socket-BEサーバーの起動 this.setupSocketBEServer(port); // イベントハンドラーの登録 this.setupEventHandlers(); } /** * MCPサーバーとツールを初期化 * @private */ private async setupMCPServer(): Promise<void> { // ツールの初期化 this.initializeTools(); // 基本ツールの登録 this.registerBasicTools(); // モジュラーツールの登録 this.registerModularTools(); // MCP Stdio Transportに接続 const transport = new StdioServerTransport(); await this.mcpServer.connect(transport); } /** * Socket-BEサーバーを起動 * @private */ private setupSocketBEServer(port: number): void { // Socket-BE Minecraftサーバーを起動 this.socketBE = new SocketBE({ port }); // MCPモードでない場合のみstderrにログ出力 if (process.stdin.isTTY !== false) { console.error( `SocketBE Minecraft WebSocketサーバーを起動中 ポート:${port}` ); console.error(`Minecraftから接続: /connect localhost:${port}/ws`); } } /** * Socket-BEイベントハンドラーを登録 * @private */ private setupEventHandlers(): void { if (!this.socketBE) return; this.socketBE.on(ServerEvent.Open, () => { this.handleServerOpen(); }); this.socketBE.on(ServerEvent.PlayerJoin, async (ev: any) => { await this.handlePlayerJoin(ev); }); this.socketBE.on(ServerEvent.PlayerLeave, (ev: any) => { this.handlePlayerLeave(ev); }); } /** * サーバーOpen時の処理 * @private */ private handleServerOpen(): void { if (process.stdin.isTTY !== false) { console.error("SocketBEサーバーが開始されました"); } // 10秒後に強制的にワールドとエージェントを設定 this.scheduleWorldInitialization(10000); // 定期的なワールドチェック(30秒ごと) this.startPeriodicWorldCheck(30000); } /** * ワールド初期化をスケジュール * @private */ private scheduleWorldInitialization(delayMs: number): void { setTimeout(async () => { try { const worlds = this.socketBE?.worlds; if (worlds && worlds instanceof Map && worlds.size > 0) { await this.initializeWorld(Array.from(worlds.values())[0]); await this.sendWorldMessage("§a[MCP Server] 接続完了!AIツールが利用可能になりました。"); } } catch (error) { // 強制設定失敗は無視してサーバー継続 } }, delayMs); } /** * 定期的なワールドチェックを開始 * @private */ private startPeriodicWorldCheck(intervalMs: number): void { setInterval(async () => { if (!this.currentWorld && this.socketBE) { const worlds = this.socketBE.worlds; if (worlds instanceof Map && worlds.size > 0) { await this.initializeWorld(Array.from(worlds.values())[0]); await this.sendWorldMessage("§a[MCP Server] 遅延接続完了!AIツールが利用可能になりました。"); } } }, intervalMs); } /** * ワールドとエージェントを初期化し、ツールに設定 * @private */ private async initializeWorld(world: World): Promise<void> { this.currentWorld = world; // エージェントを取得 try { this.currentAgent = await this.currentWorld.getOrCreateAgent(); } catch (agentError) { // エージェント取得に失敗してもサーバーは継続 this.currentAgent = null; } // 仮のプレイヤー情報を設定 if (!this.connectedPlayer) { this.connectedPlayer = { ws: null, name: "MinecraftPlayer", id: uuidv4(), }; } // 全ツールにSocket-BEインスタンスを設定 this.updateToolsWithWorldInstances(); } /** * 全ツールにワールドとエージェントを設定 * @private */ private updateToolsWithWorldInstances(): void { this.tools.forEach((tool) => { tool.setSocketBEInstances(this.currentWorld, this.currentAgent); }); } /** * ワールドにメッセージを送信(エラー無視) * @private */ private async sendWorldMessage(message: string): Promise<void> { try { await this.currentWorld?.sendMessage(message); } catch (messageError) { // メッセージ送信失敗は無視 } } /** * プレイヤー参加時の処理 * @private */ private async handlePlayerJoin(ev: any): Promise<void> { if (process.stdin.isTTY !== false) { console.error("新しいプレイヤーが参加しました:", ev.player.name); } // Minecraft側に参加確認メッセージを送信 await this.sendWorldMessage( `§b[MCP Server] §f${ev.player.name}さん、ようこそ!AIアシスタントが利用可能です。` ); this.connectedPlayer = { ws: null, // SocketBEではws直接アクセス不要 name: ev.player.name || "unknown", id: uuidv4(), }; this.currentWorld = ev.world; // エージェントを取得 try { if (this.currentWorld) { this.currentAgent = await this.currentWorld.getOrCreateAgent(); } } catch (error) { console.error("Failed to get or create agent:", error); this.currentAgent = null; } // 全ツールのSocket-BEインスタンスを更新 this.updateToolsWithWorldInstances(); } /** * プレイヤー退出時の処理 * @private */ private handlePlayerLeave(ev: any): void { if (process.stdin.isTTY !== false) { console.error(`プレイヤーが切断されました: ${ev.player.name}`); } this.connectedPlayer = null; this.currentWorld = null; this.currentAgent = null; // 全ツールのSocket-BEインスタンスをクリア this.tools.forEach((tool) => { tool.setSocketBEInstances(null, null); }); } /** * 利用可能なツールを初期化します * * Level 1(基本操作)とLevel 2(複合操作)のツールを登録し、 * 各ツールにコマンド実行関数を注入します。 * * @internal */ private initializeTools(): void { this.tools = [ // Socket-BE Core API ツール(推奨 - シンプルでAI使いやすい) new AgentTool(), new WorldTool(), new PlayerTool(), new BlocksTool(), new SystemTool(), new CameraTool(), new SequenceTool(), new MinecraftWikiTool(), // Advanced Building ツール(高レベル建築機能) new BuildCubeTool(), // ✅ 完全動作 new BuildLineTool(), // ✅ 完全動作 new BuildSphereTool(), // ✅ 完全動作 new BuildCylinderTool(), // ✅ 修正済み new BuildParaboloidTool(), // ✅ 基本動作 new BuildHyperboloidTool(), // ✅ 基本動作 new BuildRotateTool(), // ✅ 基本動作 new BuildTransformTool(), // ✅ 基本動作 new BuildTorusTool(), // ✅ 修正完了 new BuildHelixTool(), // ✅ 修正完了 new BuildEllipsoidTool(), // ✅ 修正完了 new BuildBezierTool(), // ✅ 新規追加(可変制御点ベジェ曲線) ]; // 全ツールにコマンド実行関数とSocket-BEインスタンスを設定 const commandExecutor = async ( command: string ): Promise<ToolCallResult> => { return this.executeCommand(command); }; this.tools.forEach((tool) => { tool.setCommandExecutor(commandExecutor); tool.setSocketBEInstances(this.currentWorld, this.currentAgent); }); // SequenceToolにツールレジストリを設定 const sequenceTool = this.tools.find( (tool) => tool.name === "sequence" ) as SequenceTool; if (sequenceTool) { const toolRegistry = new Map<string, BaseTool>(); this.tools.forEach((tool) => { toolRegistry.set(tool.name, tool); }); sequenceTool.setToolRegistry(toolRegistry); } } /** * MCP SDKに基本ツールを登録 */ private registerBasicTools(): void { // send_message ツール this.mcpServer.registerTool( "send_message", { title: "Send Message", description: "Send a chat message to the connected Minecraft player. ALWAYS provide a message parameter. Use this to communicate with the player about build progress or instructions.", inputSchema: { message: z .string() .describe( "The text message to send to the player (REQUIRED - never call this without a message)" ), }, }, async ({ message }: { message: string }) => { const result = await this.sendMessage(message || "Hello from MCP server!"); let responseText: string; if (result.success) { responseText = result.message || "Message sent successfully"; } else { // エラーメッセージにヒントを追加 const errorMsg = result.message || "Failed to send message"; responseText = `❌ ${enrichErrorWithHints(errorMsg)}`; } return { content: [ { type: "text", text: responseText, }, ], }; } ); // execute_command ツール this.mcpServer.registerTool( "execute_command", { title: "Execute Command", description: "Execute a Minecraft command", inputSchema: { command: z.string().describe("The Minecraft command to execute"), }, }, async ({ command }: { command: string }) => { const result = await this.executeCommand(command); // トークン最適化: コマンド結果を要約 const optimized = optimizeCommandResult(result.data); let responseText: string; if (result.success) { responseText = `✅ ${optimized.summary}`; if (optimized.details) { responseText += `\n\n${JSON.stringify(optimized.details, null, 2)}`; } } else { // エラーメッセージにヒントを追加 const errorMsg = result.message || "Command execution failed"; const enrichedError = enrichErrorWithHints(errorMsg); responseText = `❌ ${enrichedError}`; } // レスポンスサイズチェック const sizeWarning = checkResponseSize(responseText); if (sizeWarning) { responseText += `\n\n${sizeWarning}`; } return { content: [ { type: "text", text: responseText, }, ], }; } ); } /** * MCP SDKにモジュラーツールを登録 */ private registerModularTools(): void { const schemaConverter = new SchemaToZodConverter(); this.tools.forEach((tool) => { // inputSchemaをZod形式に変換(SchemaToZodConverterを使用) const zodSchema = schemaConverter.convert(tool.inputSchema); // ツールを登録 this.mcpServer.registerTool( tool.name, { title: tool.name, description: tool.description, inputSchema: zodSchema, }, async (args: any) => { try { const result = await tool.execute(args); let responseText: string; if (result.success) { // 建築ツールの場合は最適化 if (tool.name.startsWith('build_')) { const optimized = optimizeBuildResult(result); responseText = `✅ ${optimized.message}`; if (optimized.summary) { responseText += `\n\n📊 Summary:\n${JSON.stringify(optimized.summary, null, 2)}`; } } else { // 通常ツールの場合 responseText = result.message || `Tool ${tool.name} executed successfully`; if (result.data) { // データサイズチェック const dataStr = JSON.stringify(result.data, null, 2); const sizeWarning = checkResponseSize(dataStr); if (sizeWarning) { // 大きすぎる場合はデータタイプのみ表示 responseText += `\n\n${sizeWarning}`; responseText += `\nData type: ${Array.isArray(result.data) ? `Array[${result.data.length}]` : typeof result.data}`; } else { responseText += `\n\nData: ${dataStr}`; } } } } else { // エラーメッセージにヒントを追加 const errorMsg = result.message || "Tool execution failed"; const enrichedError = enrichErrorWithHints(errorMsg); responseText = `❌ ${enrichedError}`; if (result.data) { responseText += `\n\nDetails:\n${JSON.stringify(result.data, null, 2)}`; } } return { content: [ { type: "text", text: responseText, }, ], }; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error ? error.stack : undefined; const exceptionMessage = `Tool execution failed with exception: ${errorMsg}${errorStack ? `\n\nStack trace:\n${errorStack}` : ""}`; return { content: [ { type: "text", text: `❌ ${exceptionMessage}`, }, ], }; } } ); }); } private lastCommandResponse: any = null; /** * 接続中のMinecraftプレイヤーにメッセージを送信します * * @param text - 送信するメッセージテキスト * @returns 送信結果 * * @example * ```typescript * const result = server.sendMessage("Hello, Minecraft!"); * if (result.success) { * console.log("メッセージ送信成功"); * } * ``` */ public async sendMessage(text: string): Promise<ToolCallResult> { if (!this.currentWorld) { if (process.stdin.isTTY !== false) { console.error("エラー: プレイヤーが接続されていません"); } return { success: false, message: "No player connected" }; } try { if (process.stdin.isTTY !== false) { console.error(`メッセージ送信: ${text}`); } await this.currentWorld.sendMessage(text); return { success: true, message: "Message sent successfully" }; } catch (error) { if (process.stdin.isTTY !== false) { console.error("メッセージ送信エラー:", error); } return { success: false, message: `Failed to send message: ${error}` }; } } /** * Minecraftコマンドを実行します * * @param command - 実行するMinecraftコマンド("/"プレフィックスなし) * @returns コマンド実行結果 * * @example * ```typescript * // プレイヤーをテレポート * server.executeCommand("tp @p 100 64 200"); * * // ブロックを設置 * server.executeCommand("setblock 0 64 0 minecraft:stone"); * ``` */ public async executeCommand(command: string): Promise<ToolCallResult> { if (!this.currentWorld) { return { success: false, message: "No player connected" }; } try { const result = await this.currentWorld.runCommand(command); // レスポンスをlastCommandResponseに保存(位置情報取得などで使用) this.lastCommandResponse = result; return { success: true, message: "Command executed successfully", data: result, }; } catch (error) { return { success: false, message: `Command execution failed: ${error}`, }; } } /** * 最新のコマンドレスポンスを取得します(位置情報など) */ public getLastCommandResponse(): any { return this.lastCommandResponse; } } // サーバーを開始 const server = new MinecraftMCPServer(); // ポート番号をコマンドライン引数から取得 const getPort = (): number => { // コマンドライン引数から取得 (--port=8002) const portArg = process.argv.find((arg) => arg.startsWith("--port=")); if (portArg) { const port = parseInt(portArg.split("=")[1]); if (!isNaN(port) && port > 0 && port <= 65535) { return port; } } // デフォルト値 return 8001; }; // 言語設定をコマンドライン引数から取得 const getLocale = (): SupportedLocale | undefined => { // コマンドライン引数から取得 (--lang=ja または --lang=en) const langArg = process.argv.find((arg) => arg.startsWith("--lang=")); if (langArg) { const lang = langArg.split("=")[1]; if (lang === "ja" || lang === "en") { return lang; } } // デフォルトは自動検出(undefined) return undefined; }; const port = getPort(); const locale = getLocale(); server.start(port, locale); process.on("SIGINT", () => { process.exit(0); });

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mming-Lab/minecraft-bedrock-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server