server.ts•23.5 kB
import { Server as SocketBE, ServerEvent, World, Agent } from "socket-be";
import { v4 as uuidv4 } from "uuid";
import {
MCPRequest,
MCPResponse,
Tool,
ConnectedPlayer,
ToolCallResult,
} from "./types";
// レベル1: 基本操作ツール(Socket-BE移行済み)
// 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";
/**
* 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;
/**
* 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 start(port: number = 8001, locale?: SupportedLocale): void {
// 言語設定を初期化
initializeLocale(locale);
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`);
}
this.socketBE.on(ServerEvent.Open, () => {
if (process.stdin.isTTY !== false) {
console.error("SocketBEサーバーが開始されました");
}
// 代替手段: 10秒後に強制的にワールドとエージェントを設定(ワールド登録待ち)
setTimeout(async () => {
try {
// Socket-BEからワールドを取得
const worlds = this.socketBE?.worlds;
if (worlds && worlds instanceof Map && worlds.size > 0) {
this.currentWorld = Array.from(worlds.values())[0];
// エージェントを取得
try {
if (this.currentWorld) {
this.currentAgent = await this.currentWorld.getOrCreateAgent();
}
} catch (agentError) {
// エージェント取得に失敗してもサーバーは継続
}
// 仮のプレイヤー情報を設定
this.connectedPlayer = {
ws: null,
name: "MinecraftPlayer",
id: uuidv4(),
};
// 全ツールにSocket-BEインスタンスを設定
this.tools.forEach((tool) => {
tool.setSocketBEInstances(this.currentWorld, this.currentAgent);
});
// Minecraft側に接続確認メッセージを送信
try {
await this.currentWorld.sendMessage(
"§a[MCP Server] 接続完了!AIツールが利用可能になりました。"
);
} catch (messageError) {
// メッセージ送信失敗は無視
}
}
} catch (error) {
// 強制設定失敗は無視してサーバー継続
}
}, 10000);
// 定期的なワールドチェック(30秒ごと)
setInterval(async () => {
if (!this.currentWorld && this.socketBE) {
const worlds = this.socketBE.worlds;
if (worlds instanceof Map && worlds.size > 0) {
this.currentWorld = Array.from(worlds.values())[0];
try {
if (this.currentWorld) {
this.currentAgent = await this.currentWorld.getOrCreateAgent();
this.tools.forEach((tool) => {
tool.setSocketBEInstances(
this.currentWorld,
this.currentAgent
);
});
await this.currentWorld.sendMessage(
"§a[MCP Server] 遅延接続完了!AIツールが利用可能になりました。"
);
}
} catch (delayedError) {
// 遅延設定失敗は無視
}
}
}
}, 30000);
});
this.socketBE.on(ServerEvent.PlayerJoin, async (ev: any) => {
if (process.stdin.isTTY !== false) {
console.error("新しいプレイヤーが参加しました:", ev.player.name);
}
// Minecraft側に参加確認メッセージを送信
try {
await ev.world.sendMessage(
`§b[MCP Server] §f${ev.player.name}さん、ようこそ!AIアシスタントが利用可能です。`
);
} catch (messageError) {
// メッセージ送信失敗は無視
}
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.tools.forEach((tool) => {
tool.setSocketBEInstances(this.currentWorld, this.currentAgent);
});
});
this.socketBE.on(ServerEvent.PlayerLeave, (ev: any) => {
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);
});
});
// MCP stdin処理
this.setupMCPInterface();
// ツールの初期化
this.initializeTools();
}
/**
* 利用可能なツールを初期化します
*
* 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);
}
}
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;
}
/**
* MCPインターフェースを設定します
* Claude Desktop等のMCPクライアントとの通信を初期化
*/
private setupMCPInterface(): void {
// Claude Desktop用にMCPインターフェースを常に設定
process.stdin.setEncoding("utf8");
process.stdin.on("data", async (data: string) => {
const line = data.toString().trim();
if (!line) return;
try {
const request: MCPRequest = JSON.parse(line);
const response = await this.handleMCPRequest(request);
// 通知以外の全リクエストに対して即座にレスポンスを送信
if (response !== null) {
process.stdout.write(JSON.stringify(response) + "\n");
}
} catch (error) {
// 無効なJSONに対するエラーレスポンスを送信
if (line.includes('"id"')) {
try {
const partialRequest = JSON.parse(line);
if (partialRequest.id !== undefined) {
const errorResponse: MCPResponse = {
jsonrpc: "2.0",
id: partialRequest.id,
error: {
code: -32700,
message: "Parse error",
},
};
process.stdout.write(JSON.stringify(errorResponse) + "\n");
}
} catch (e) {
// IDを抽出できない場合は無視
}
}
}
});
}
/**
* MCPリクエストを処理します
*
* JSON-RPC 2.0形式のリクエストを解析し、適切なレスポンスを生成します。
*
* @param request - MCPリクエスト
* @returns MCPレスポンス(通知の場合はnull)
* @internal
*/
private async handleMCPRequest(
request: MCPRequest
): Promise<MCPResponse | null> {
switch (request.method) {
case "notifications/initialized":
case "notifications/cancelled":
// 通知はレスポンスが不要
return null;
case "initialize":
return {
jsonrpc: "2.0",
id: request.id,
result: {
protocolVersion: "2024-11-05",
capabilities: { tools: {} },
serverInfo: {
name: "mcbk-mcp-typescript",
version: "1.0.0",
},
},
};
case "tools/list":
return {
jsonrpc: "2.0",
id: request.id,
result: {
tools: this.getTools(),
},
};
case "tools/call":
return await this.handleToolCall(request);
case "resources/list":
return {
jsonrpc: "2.0",
id: request.id,
result: {
resources: [],
},
};
case "prompts/list":
return {
jsonrpc: "2.0",
id: request.id,
result: {
prompts: [],
},
};
default:
return {
jsonrpc: "2.0",
id: request.id,
error: { code: -32601, message: "Method not found" },
};
}
}
/**
* 利用可能なツール一覧を取得します
* MCPクライアントにツール情報を提供
* @returns ツール定義の配列
*/
private getTools(): Tool[] {
const basicTools: Tool[] = [
{
name: "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: {
type: "object",
properties: {
message: {
type: "string",
description:
"The text message to send to the player (REQUIRED - never call this without a message)",
},
},
required: ["message"],
},
},
{
name: "execute_command",
description: "Execute a Minecraft command",
inputSchema: {
type: "object",
properties: {
command: {
type: "string",
description: "The Minecraft command to execute",
},
},
required: ["command"],
},
},
];
// モジュラーツールを追加
const modularTools: Tool[] = this.tools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
}));
return [...basicTools, ...modularTools];
}
/**
* ツール実行リクエストを処理します
* MCPクライアントからのツール呼び出しを受け取り、適切なツールに委譲
* @param request - MCPツール実行リクエスト
* @returns ツール実行結果のMCPレスポンス
*/
private async handleToolCall(request: MCPRequest): Promise<MCPResponse> {
try {
const toolName = request.params?.name;
const args = request.params?.arguments || request.params?.args || {};
if (!toolName) {
return {
jsonrpc: "2.0",
id: request.id,
error: { code: -32602, message: "Missing tool name" },
};
}
let result: ToolCallResult;
// 基本ツールの処理
if (toolName === "send_message") {
// メッセージパラメータの処理
const message = args.message || args.text || "Hello from MCP server!";
if (!message || message.trim() === "") {
return {
jsonrpc: "2.0",
id: request.id,
error: {
code: -32602,
message: "Message parameter is required and cannot be empty",
},
};
}
result = await this.sendMessage(message);
} else if (toolName === "execute_command") {
result = await this.executeCommand(args.command);
} else {
// モジュラーツールの処理
const tool = this.tools.find((t) => t.name === toolName);
if (!tool) {
return {
jsonrpc: "2.0",
id: request.id,
error: { code: -32603, message: `Unknown tool: ${toolName}` },
};
}
try {
result = await tool.execute(args);
} catch (error) {
// ツール実行中の例外をキャッチして詳細を返す
const errorMsg =
error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
console.error(
`[server] Tool ${toolName} threw exception: ${errorMsg}`
);
if (errorStack) {
console.error(`[server] Stack trace:\n${errorStack}`);
}
const exceptionMessage = `Tool execution failed with exception: ${errorMsg}${errorStack ? `\n\nStack trace:\n${errorStack}` : ""}`;
// Claude Desktopはerror.messageフィールドを無視し固定の"Tool execution failed"を表示するため、
// エラーメッセージをresult.contentとして返す(回避策)
// MCP仕様では非準拠だが、ユーザーに詳細なエラー情報を提供するために必要
return {
jsonrpc: "2.0",
id: request.id,
result: {
content: [
{
type: "text",
text: `❌ ${exceptionMessage}`
}
]
}
};
}
}
if (!result.success) {
// ツール実行失敗時により詳細なエラーメッセージを提供
const errorMessage = result.message || "Tool execution failed";
const detailedMessage = result.data
? `${errorMessage}\n\nDetails:\n${JSON.stringify(result.data, null, 2)}`
: errorMessage;
console.error(`[server] Tool ${toolName} failed: ${detailedMessage}`);
// Claude Desktopはerror.messageフィールドを無視し固定の"Tool execution failed"を表示するため、
// エラーメッセージをresult.contentとして返す(回避策)
// MCP仕様では非準拠だが、ユーザーに詳細なエラー情報を提供するために必要
return {
jsonrpc: "2.0",
id: request.id,
result: {
content: [
{
type: "text",
text: `❌ ${detailedMessage}`
}
]
}
};
}
return {
jsonrpc: "2.0",
id: request.id,
result: {
content: [
{
type: "text",
text: result.data
? `${result.message || `Tool ${toolName} executed successfully`}\n\nData: ${JSON.stringify(result.data, null, 2)}`
: result.message || `Tool ${toolName} executed successfully`,
},
],
},
};
} catch (error) {
return {
jsonrpc: "2.0",
id: request.id,
error: {
code: -32603,
message: `Tool call handler error: ${error instanceof Error ? error.message : String(error)}`,
},
};
}
}
}
// サーバーを開始
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);
});