UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

474 lines (473 loc) 22.3 kB
import * as http from "http"; import { z } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import CreatorTools from "../app/CreatorTools"; import ServerManager from "./ServerManager"; import LocalEnvironment from "./LocalEnvironment"; import { IWorldSettings } from "../minecraft/IWorldSettings"; import IStatus from "../app/Status"; import IActionSetData from "../actions/IActionSetData"; import IVector3 from "../minecraft/IVector3"; import Project from "../app/Project"; import { ModelTemplateType } from "../minecraft/ModelDesignTemplates"; import { MinecraftContentSchema } from "../minecraft/ContentMetaSchemaZod"; /** * Interface for MCT MCP preferences that can be stored in .mct/mcp/prefs.json files. * These preferences control security and feature flags for MCP operations. */ export interface IMctMcpPrefs { /** If true, allows the readImageFile tool to read image files from this folder and its subfolders */ allowImageFileReadsInDescendentFolders?: boolean; /** If true, allows the writeImageFile tool to write image files to this folder and its subfolders */ allowImageFileWritesInDescendentFolders?: boolean; } export default class MinecraftMcpServer { /** Starting port for the internal HTTP server range */ private static readonly PORT_RANGE_START; /** Ending port for the internal HTTP server range (200 ports available) */ private static readonly PORT_RANGE_END; /** Maximum attempts to find an available port before giving up */ private static readonly PORT_MAX_ATTEMPTS; private _server; private _env; /** Single HTTP transport instance. Created once in startHttp() and reused for all requests. */ private _httpTransport; private _creatorTools; private _serverManager; /** Cache for loaded MCP preferences, keyed by folder path where prefs.json was found */ private _mcpPrefsCache; /** Folders we've already checked and found no prefs.json (negative cache) */ private _mcpPrefsNotFoundFolders; /** HTTP server port for model preview rendering (dynamically assigned on startup) */ private _previewServerPort; /** * Cached PlaywrightPageRenderer for reuse across preview operations. * * TODO: The renderer init/health-check/reinit boilerplate is duplicated ~4 times * in this file (preview_model, preview_volume, preview_structure, screenshot). * Extract into a shared `ensureRendererReady(baseUrl, httpServer)` method. */ private _cachedRenderer; /** Flag to prevent multiple cleanup calls */ private _cleaningUp; /** * Maps user-facing session names to BDS slot numbers. * The "default" session is auto-registered when `mct serve` starts BDS on slot 0. * Additional sessions can be created via createMinecraftSessionWithContent or * connected to existing slots via connectToMinecraftSession. */ private _sessions; /** * Working folder path for MCP operations. * When set via the -i argument, this folder is used as the default context for * file operations and is exposed to AI assistants via the MCP protocol's prompts. */ private _workingFolder; /** Getter for the working folder */ get workingFolder(): string | undefined; constructor(); /** * Wrapper for McpServer.registerTool that avoids TS2589 "Type instantiation is * excessively deep and possibly infinite" caused by the SDK's complex generic * inference on ToolCallback<InputArgs>. Casts the callback to `any` to break the * recursive type chain while preserving runtime behavior. */ private _registerTool; /** * Ensures a ServerManager instance is available, creating one if needed. * If a ServerManager was provided externally (e.g., from HttpServer in `mct serve` mode), * it will be reused. Otherwise, a new one is created with the "mcp" slot prefix * to isolate MCP servers from other contexts. */ private ensureServerManager; /** * Resolve a session name to a slot number. * - If sessionName is empty/undefined or "default", returns slot 0 (the default session). * - Otherwise, looks up the session in the registered sessions map. * - Throws if the session name is not found. */ private _resolveSlot; /** * Handle an incoming HTTP request by delegating to the single transport. * * Architecture: We use one StreamableHTTPServerTransport created in startHttp() * and connected once to the McpServer. The transport handles session management, * initialization, and request routing internally. This avoids the SDK limitation * where registerCapabilities() cannot be called after connect(). * * For POST requests, we pre-parse the body since Node's http.IncomingMessage * doesn't auto-parse JSON (unlike Express). The parsed body is passed to * transport.handleRequest() so it doesn't try to re-parse. */ handleRequest(req: http.IncomingMessage, res: http.ServerResponse<http.IncomingMessage>): Promise<void>; sendErrorRequest(statusCode: number, message: string, req: http.IncomingMessage, res: http.ServerResponse): void; /** * Finds MCP preferences for a given file path by looking for .mct/mcp/prefs.json * in the file's parent folder and up to 11 levels of parent folders. * Results are cached per session to avoid redundant file system reads. * * @param filePath The absolute path to the file being accessed * @returns The MCP preferences if found, or undefined if no prefs.json exists` */ getMcpPrefsForPath(filePath: string): IMctMcpPrefs | undefined; /** * Checks if a specific MCP preference flag is enabled for a given file path. * * @param filePath The absolute path to the file being accessed * @param prefKey The preference key to check * @returns true if the preference is explicitly set to true, false otherwise */ isMcpPrefEnabled(filePath: string, prefKey: keyof IMctMcpPrefs): boolean; /** * Validates that a file path is safe for MCP operations. * Ensures the path doesn't use traversal sequences and that the resolved path * stays within the directory tree authorized by the prefs.json file. */ private _validateMcpFilePath; /** * Validates an MCP file path and returns an error CallToolResult if invalid, * or undefined if the path is safe. Use as an early-return guard in MCP tool handlers. */ private _checkMcpFilePath; _processValidateContent(args: { jsonContentOrBase64ZipContent: string; }): Promise<CallToolResult>; _sessionOp(args: { sessionName: string; }): Promise<CallToolResult>; /** * Lists all Minecraft sessions — both registered named sessions and any * active BDS slots discovered via the ServerManager. */ _listMinecraftSessionsOp(): Promise<CallToolResult>; /** * Registers an existing BDS slot as a named session so that subsequent * tool calls can reference it by name. */ _connectToMinecraftSessionOp(args: { sessionName: string; slot?: number; }): Promise<CallToolResult>; _moveSessionPlayerToLocation(args: { sessionName: string; playerName: string; locationToHavePlayerMoveTo: IVector3; }): Promise<CallToolResult>; _createMinecraftSession(args: { sessionName: string; packagedMcaddonOrMcworldFilePath: string; testPlayerNameToUse: string; }): Promise<CallToolResult>; _runActionSet(actionSet: IActionSetData, slot?: number): Promise<any>; getWorldSettings(): IWorldSettings; _runActionSetOp(args: { actionSet?: any; sessionName?: string; }): Promise<CallToolResult>; _runCommandOp(args: { sessionName: string; command: string; }): Promise<CallToolResult>; runCommand(command: string, token?: string, slot?: number): Promise<string | undefined>; _processValidateContentAtPath(args: { filePath: string; }): Promise<CallToolResult>; _create(project: Project, title: string, description: string, newName: string, creator: string, template: string): Promise<void>; _add(project: Project, templateType: string, newName: string): Promise<boolean>; _createOp(args: { folderPathToCreateProjectAt: string; title: string; description?: string; newName: string; creator: string; template: "addonStarter" | "tsStarter" | "addonFull" | "scriptBox" | "dlStarter" | "editor-scriptBox" | "editor-basics"; }): Promise<CallToolResult>; _addOp(args: { folderPathToCreateProjectAt: string; templateType: "basicUnitCubeBlock" | "crateBlock" | "basicDieBlock" | "sushiRollBlock" | "fishBowlBlock" | "hardBiscuit" | "pear" | "elixir" | "rod" | "key" | "customSword" | "wrench" | "allay" | "axolotl" | "cat" | "cow" | "creeper" | "enderman" | "rabbit" | "pig" | "sheep" | "skeleton" | "wolf" | "zombie" | "spawn_rule" | "loot_table" | "recipe_shapeless" | "recipe_shaped" | "feature_rule" | "jigsaw" | "atmospherics" | "color_grading" | "lighting" | "pbr" | "biome_behavior" | "entity_behavior" | "entity_resources" | "item_behavior" | "attachable" | "block_behavior" | "block_culling" | "block_catalog" | "biome_resource" | "aggregate_feature" | "animation" | "animation_controller" | "render_controller"; name: string; }): Promise<CallToolResult>; /** * Creates Minecraft content from a meta-schema definition. * This is a simplified, AI-friendly format that generates all required files. */ _createMinecraftContentOp(args: { definition: z.infer<typeof MinecraftContentSchema>; outputPath: string; }): Promise<CallToolResult>; /** * Finds the first existing pack folder inside a container directory (e.g., behavior_packs/). * Returns the folder path if a pack (folder with manifest.json) is found, otherwise undefined. */ private static _findExistingPackFolder; /** * Returns true when the supplied folder already looks like a Bedrock resource * pack — i.e. it contains a top-level manifest.json whose modules include a * `resources` module. Used by designModel to avoid creating a nested * `resource_packs/` subdirectory inside an RP that the caller passed * directly, which previously caused a recurring "files written to the wrong * path" symptom. */ private static _isResourcePackFolder; /** * Sanitize a display name or namespace into a safe folder name. * Lower-cases, replaces non-alphanumeric runs with '_', trims. * Caps length at 64 characters to keep resulting paths well under the * Windows MAX_PATH limit once nested inside the project/pack hierarchy. */ private static _toSafeFolderName; /** * Returns the list of visible child entries in a directory, or an empty array if * the directory doesn't exist / can't be read. Hidden/system files (.git, .DS_Store, * Thumbs.db, desktop.ini) are ignored when deciding whether a folder is "empty". */ private static _visibleChildren; /** * Resolves where content should actually be generated, given a user-provided outputPath. * * Heuristic (in priority order): * 1. If outputPath (or a parent within 2 levels) has a Minecraft project reference point * — package.json, behavior_packs/, resource_packs/, or a manifest.json — anchor to * that project root. This is the most common "add to existing project" case. * 2. If outputPath doesn't exist, or is empty (ignoring hidden/system files), use it * directly as the project root. * 3. Otherwise (outputPath is non-empty with unrelated content), create a subfolder * named after the namespace/displayName and use that as the root. * * Returns both the resolved root and a human-readable reason string (useful for the * tool response so agents learn where files landed and why). */ private static _resolveProjectRoot; /** * Writes a manifest.json file, but preserves the UUIDs from any existing manifest * at the same location. This prevents breaking worlds that already reference the pack * when content is added across multiple MCP calls. */ private static _writeManifestPreservingUuids; /** * Writes a singleton JSON file (terrain_texture.json, item_texture.json, blocks.json, * sound_definitions.json, music_definitions.json, etc.), deep-merging with any existing * data so that previously-added entries are preserved instead of being overwritten. * * Merge strategy: * - Recursively merges object keys: new entries win on conflict, but existing * object-valued entries are recursively merged rather than replaced. * - Scalar top-level keys from the existing file are preserved if absent in the new content. * - This handles texture_data in terrain/item_texture.json, block entries in blocks.json, * entity_sounds.entities in sounds.json, sound_definitions entries, etc. */ private static _writeSingletonJsonMerging; /** * Gets the effective content schema for an existing Minecraft project. * This analyzes the project's entities, blocks, items, etc. and infers what * traits and simplified properties would represent them in meta-schema format. * * This is the inverse of createMinecraftContentOp - instead of generating * native content from a schema, it analyzes native content to produce a schema. */ _getEffectiveContentSchemaOp(args: { folderPath: string; options?: { minTraitConfidence?: number; includeRawComponents?: boolean; inferNamespace?: boolean; includeBehaviorPresets?: boolean; includeComponentGroups?: boolean; includeEvents?: boolean; }; }): Promise<CallToolResult>; /** * Reads an image file and returns its contents as base64-encoded image data * that can be displayed by the AI. * * Requires allowImageFileReadsInDescendentFolders to be set to true in * .mct/mcp/prefs.json in the file's parent folder or up to 3 levels above. */ _readImageFileOp(args: { filePath: string; }): Promise<CallToolResult>; /** * Extract dimensions from a JPEG buffer by scanning for SOF markers. * Returns a "widthxheight" string, or empty string if extraction fails. */ private static _getJpegDimensions; /** * Maximum base64 size for images returned from MCP tools to stay safely under * API request limits (e.g., Claude's ~4MB total request limit). * Base64 encoding inflates binary data by ~33%, and we need headroom for the rest * of the request payload (tool definitions, conversation history, etc.), so we * cap individual images at ~1.5MB base64. This is more conservative than the 3MB * limit to account for conversations with multiple images. */ private static readonly MAX_IMAGE_BASE64_BYTES; /** * Ensure a base64-encoded image fits within AI context limits. * If the image is too large, it will be downscaled and re-encoded as PNG. * * This should be called before returning any image from an MCP tool to prevent * 413 Request Entity Too Large errors from the AI backend. * * @param base64Data Base64-encoded image data * @param mimeType MIME type of the image * @returns Object with (possibly downscaled) base64 data and mimeType. The mimeType * may change to "image/png" if the image was re-encoded. */ static ensureImageFitsContext(base64Data: string, mimeType: string): { base64: string; mimeType: string; wasDownscaled: boolean; }; /** * Downscale an image buffer so its base64 representation fits within maxBase64Bytes. * Decodes the image to RGBA pixels, computes a scale factor, resizes using bilinear * interpolation, and re-encodes as PNG. * * @param imageBuffer Raw image file bytes (PNG, JPEG, etc.) * @param mimeType MIME type of the source image * @param maxBase64Bytes Maximum allowed base64 string length * @returns Object with base64 string, or undefined if downscaling fails */ private static _downscaleImageToFit; /** * Resize RGBA pixel data using bilinear interpolation. * Produces smoother results than nearest-neighbor for downscaling screenshots and textures. * * @param srcPixels Source RGBA pixel data (4 bytes per pixel) * @param srcW Source width * @param srcH Source height * @param dstW Destination width * @param dstH Destination height * @returns Resized RGBA pixel data */ private static _bilinearResize; /** * Writes base64-encoded image data to a file. * * Requires allowImageFileWritesInDescendentFolders to be set to true in * .mct/mcp/prefs.json in the file's parent folder or up to 3 levels above. */ _writeImageFileFromBase64Op(args: { filePath: string; base64Data: string; mimeType?: string; }): Promise<CallToolResult>; /** * Converts SVG markup to PNG and writes it to a file. * * Requires allowImageFileWritesInDescendentFolders to be set to true in * .mct/mcp/prefs.json in the file's parent folder or up to 3 levels above. */ _writeImageFileFromSvgOp(args: { filePath: string; svgContent: string; width?: number; height?: number; }): Promise<CallToolResult>; /** * Writes an image file from a pixel art definition with paletted pixels. * * The pixel art format uses ASCII-style lines where each character maps to a color * in a palette. Spaces are transparent. This is ideal for creating Minecraft-style * pixel art textures. * * Requires allowImageFileWritesInDescendentFolders to be set to true in * .mct/mcp/prefs.json in the file's parent folder or up to 3 levels above. */ _writeImageFileFromPixelArtOp(args: { filePath: string; lines: string[]; palette: { [char: string]: { r?: number; g?: number; b?: number; a?: number; hex?: string; }; }; scale?: number; backgroundColor?: { r?: number; g?: number; b?: number; a?: number; hex?: string; }; }): Promise<CallToolResult>; /** * Preview a model design by converting it to geometry and rendering a preview image. * Returns the preview as a base64-encoded PNG image. */ _previewModelDesignOp(args: any): Promise<CallToolResult>; /** * Export a model design to .geo.json and texture.png files. */ _exportModelDesignOp(args: any): Promise<CallToolResult>; /** * Unified tool: Creates a 3D model, exports files to project, and returns a preview. * * This combines the functionality of previewModelDesign and exportModelDesign into * a single, project-aware operation that: * 1. Validates and converts the design to geometry + texture * 2. Saves files to the appropriate project folder (auto-detected) * 3. Persists the design to an accessory folder for future iteration * 4. Auto-wires to matching entity/block/item if found * 5. Returns a preview image */ _designModelOp(args: any): Promise<CallToolResult>; /** * Unified tool for building structures in Minecraft projects. * Combines structure preview + export + project integration into one step. */ _designStructureOp(args: any): Promise<CallToolResult>; /** * Returns starter model templates for common Minecraft entity types. * These provide proper Minecraft-scale geometry with blocky pixel-art style textures. */ _getModelTemplatesOp(args: { templateType: ModelTemplateType; }): Promise<CallToolResult>; /** * Validates an IBlockVolume has the required basic structure. * Size is now optional - if not provided, it will be inferred from the data. * String lengths and row counts don't need to match exactly - shorter strings * and missing rows are treated as air blocks. * * Returns an error message if validation fails, or undefined if valid. */ private _validateBlockVolumeDimensions; /** * Preview a structure design (IBlockVolume) by converting it to an MCStructure and rendering a preview image. * Returns the preview as a base64-encoded PNG image from multiple angles. */ _previewStructureDesignOp(args: any): Promise<CallToolResult>; /** * Export a structure design (IBlockVolume) to an MCStructure file. */ _exportStructureDesignOp(args: any): Promise<CallToolResult>; /** * Configure MCP prompts that expose server configuration to AI assistants. * The main prompt is "working-folder" which tells the AI where to write content. */ _configurePrompts(): void; _configureTools(): Promise<void>; /** * Check if a port is available for use. * @param port The port number to check * @returns Promise that resolves to true if the port is available, false otherwise */ private static isPortAvailable; /** * Find an available port by randomly selecting from the configured range. * Excludes browser-unsafe ports that would cause ERR_UNSAFE_PORT errors. * @returns Promise that resolves to an available port, or a random port if none found after max attempts */ private findAvailablePort; startStdio(creatorTools: CreatorTools, env: LocalEnvironment, workingFolder?: string): Promise<void>; /** * Clean up resources (browser, HTTP server, etc.) */ cleanup(): Promise<void>; static handleStatusAdded(creatorTools: CreatorTools, status: IStatus): void; startHttp(creatorTools: CreatorTools, env: LocalEnvironment, serverManager?: ServerManager): Promise<void>; }