@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
474 lines (473 loc) • 22.3 kB
TypeScript
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>;
}