Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Editor/UI/MCPDebugWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ public static void ShowWindow()
wnd.minSize = new Vector2(400, 500);
}

/// <summary>
/// Initializes the debug window UI by loading visual assets, configuring UI elements, and binding event callbacks.
/// </summary>
/// <remarks>
/// This method clones the UXML layout into the window's root element and applies styling from the USS asset.
/// It queries essential UI components such as connection status labels, buttons, toggles, and text fields, and sets
/// a default server port value ("5010") if the port field is empty. Additionally, it binds events for connection,
/// disconnection, and auto-reconnect functionalities, sets up logging toggles, updates the UI to reflect the current state,
/// and registers an editor update callback. If either the UXML or USS asset is missing, an error is logged for debugging purposes.
/// </remarks>
public void CreateGUI()
{
VisualElement root = rootVisualElement;
Expand Down Expand Up @@ -106,6 +116,13 @@ public void CreateGUI()
EditorApplication.update += OnEditorUpdate;
}

/// <summary>
/// Creates a fallback user interface for the MCP Debug Window when the UXML layout is unavailable.
/// </summary>
/// <remarks>
/// This method builds a basic UI on the provided root element that includes a notification label about the missing UXML, a text field for entering the server port (default value "5010"), buttons to initiate connection and disconnection, a toggle for auto-reconnect functionality, and a label to display connection status.
/// </remarks>
/// <param name="root">The container to which the fallback UI elements are added.</param>
private void CreateFallbackUI(VisualElement root)
{
// Create a simple fallback UI if UXML fails to load
Expand Down Expand Up @@ -232,6 +249,16 @@ private void OnLoggingToggleChanged(string componentName, bool enabled)
MCPLogger.SetComponentLoggingEnabled(componentName, enabled);
}

/// <summary>
/// Initiates a connection attempt when the connect button is clicked.
/// </summary>
/// <remarks>
/// Retrieves and validates the server port from the input field, defaulting to "5010" if empty,
/// and constructs a WebSocket URI using localhost and the specified port. If a MCPConnectionManager
/// instance is found, its internal server URI is updated via reflection. Depending on the MCPManager’s
/// initialization state, the method either retries an existing connection or initializes a new connection,
/// updating the UI state accordingly. Displays a dialog for invalid port input or connection errors.
/// </remarks>
private void OnConnectClicked()
{
// Always use localhost for the WebSocket URL
Expand Down
167 changes: 161 additions & 6 deletions mcpServer/build/filesystemTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,20 @@ import { createTwoFilesPatch } from 'diff';
import { minimatch } from 'minimatch';
import { ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, EditFileArgsSchema, ListDirectoryArgsSchema, DirectoryTreeArgsSchema, SearchFilesArgsSchema, GetFileInfoArgsSchema, FindAssetsByTypeArgsSchema, ListScriptsArgsSchema } from './toolDefinitions.js';
// Helper functions
// Updated validatePath function to properly handle empty paths
/**
* Validates and normalizes a file path, ensuring it remains within the specified asset root.
*
* The function first treats empty or quote-only paths as a request for the asset root. It then cleans the path
* by removing extraneous quotes and escape characters, normalizes it, and handles relative paths by joining them with
* the asset root. For absolute paths that do not start with the asset root, it attempts to resolve them as relative paths.
* If the final resolved path escapes the asset root directory, an error is thrown.
*
* @param {string} requestedPath - The user-provided file path, which may include extraneous characters or be empty.
* @param {string} assetRootPath - The base directory that the resolved path must remain within.
* @returns {Promise<string>} A promise that resolves to the validated and normalized absolute file path.
*
* @throws {Error} If the resolved path is outside the asset root directory.
*/
async function validatePath(requestedPath, assetRootPath) {
// If path is empty or just quotes, use the asset root path directly
if (!requestedPath || requestedPath.trim() === '' || requestedPath.trim() === '""' || requestedPath.trim() === "''") {
Expand Down Expand Up @@ -49,6 +62,25 @@ async function validatePath(requestedPath, assetRootPath) {
}
return resolvedPath;
}
/**
* Retrieves metadata for the specified file.
*
* This asynchronous function obtains file statistics including size, creation,
* modification, and access times, as well as its permissions. It also indicates whether
* the provided path refers to a file or a directory.
*
* @param {string} filePath - The path to the file or directory.
* @returns {Promise<Object>} An object containing:
* - size {number}: The file size in bytes.
* - created {Date}: The file's creation time.
* - modified {Date}: The last modification time.
* - accessed {Date}: The last access time.
* - isDirectory {boolean}: True if the path is a directory.
* - isFile {boolean}: True if the path is a file.
* - permissions {string}: The file's permissions as the last three octal digits (e.g., "644").
*
* @throws {Error} If retrieving file statistics fails, such as when the file does not exist.
*/
async function getFileStats(filePath) {
const stats = await fs.stat(filePath);
return {
Expand All @@ -61,6 +93,21 @@ async function getFileStats(filePath) {
permissions: stats.mode.toString(8).slice(-3),
};
}
/**
* Recursively searches for files and directories whose names include the specified pattern,
* while excluding paths that match any provided glob patterns.
*
* Starting at the given root directory, this asynchronous function traverses the directory tree and:
* - Computes the relative path for each entry to check against the exclusion patterns.
* - Performs a case-insensitive check to see if the entry's name contains the specified search pattern.
* - Recursively explores directories that are not excluded.
* Any errors encountered during traversal are silently ignored to allow the search to continue.
*
* @param {string} rootPath - The directory to begin the search.
* @param {string} pattern - The substring to match within file and directory names (case-insensitive).
* @param {string[]} [excludePatterns=[]] - Optional array of glob patterns; paths matching these patterns are skipped.
* @returns {Promise<string[]>} A promise that resolves to an array of paths for entries that match the search pattern.
*/
async function searchFiles(rootPath, pattern, excludePatterns = []) {
const results = [];
async function search(currentPath) {
Expand Down Expand Up @@ -93,15 +140,55 @@ async function searchFiles(rootPath, pattern, excludePatterns = []) {
await search(rootPath);
return results;
}
/**
* Normalizes Windows-style carriage return and newline sequences to Unix-style newlines.
*
* Replaces all occurrences of "\r\n" in the provided text with "\n" to ensure consistent line endings.
*
* @param {string} text - The text to normalize.
* @returns {string} The text with normalized Unix-style line endings.
*/
function normalizeLineEndings(text) {
return text.replace(/\r\n/g, '\n');
}
/**
* Generates a unified diff patch showing the differences between the original and new file content.
*
* This function first normalizes line endings in both inputs to guarantee a consistent diff format,
* then creates a unified diff patch using the provided file identifier for header annotations.
*
* @param {string} originalContent - The original file content.
* @param {string} newContent - The updated file content.
* @param {string} [filepath='file'] - The file identifier used in the diff header.
* @returns {string} A unified diff string representing the changes between the two versions of content.
*/
function createUnifiedDiff(originalContent, newContent, filepath = 'file') {
// Ensure consistent line endings for diff
const normalizedOriginal = normalizeLineEndings(originalContent);
const normalizedNew = normalizeLineEndings(newContent);
return createTwoFilesPatch(filepath, filepath, normalizedOriginal, normalizedNew, 'original', 'modified');
}
/**
* Applies a series of text edits to a file and returns a formatted unified diff of the changes.
*
* This asynchronous function reads the content from the specified file, normalizes its line endings,
* and sequentially applies each edit. Each edit specifies an "oldText" to search for and a "newText"
* to substitute. The function first attempts an exact match; if not found, it then performs a
* flexible, line-by-line replacement that preserves the file's indentation. If an edit's old text
* cannot be found, an error is thrown.
*
* After applying all edits, a unified diff is generated to represent the changes. The diff is
* formatted within a code block that adapts the number of backticks based on its content. When
* dryRun is false (the default), the modified content is written back to the file; otherwise, no
* file write occurs.
*
* @param {string} filePath - The path to the file to be edited.
* @param {Array<{oldText: string, newText: string}>} edits - An array of edits describing the text to replace and its replacement.
* @param {boolean} [dryRun=false] - If true, simulates the edits without saving changes to the file.
* @returns {Promise<string>} A formatted unified diff of the changes applied.
*
* @throws {Error} If an edit's old text cannot be located in the file content.
*/
async function applyFileEdits(filePath, edits, dryRun = false) {
// Read file content and normalize line endings
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
Expand Down Expand Up @@ -164,6 +251,20 @@ async function applyFileEdits(filePath, edits, dryRun = false) {
}
return formattedDiff;
}
/**
* Recursively constructs a tree representation of the directory structure.
*
* This asynchronous function reads the contents of the directory at the specified
* path, validates it against the asset root, and recursively processes subdirectories
* up to the specified maximum depth. When the maximum depth is reached, it returns a
* stub entry to indicate that further subdirectories exist.
*
* @param {string} currentPath - The starting directory path to build the tree from, typically relative to the asset root.
* @param {string} assetRootPath - The root directory used to validate and resolve the current path.
* @param {number} [maxDepth=5] - The maximum depth the function will traverse.
* @param {number} [currentDepth=0] - The current depth level during recursion (used internally).
* @returns {Promise<Array<Object>>} A promise that resolves to an array representing the directory tree. Each object includes a "name" and a "type" (either "file" or "directory"), and directory objects may include a "children" property with nested entries.
*/
async function buildDirectoryTree(currentPath, assetRootPath, maxDepth = 5, currentDepth = 0) {
if (currentDepth >= maxDepth) {
return [{ name: "...", type: "directory" }];
Expand All @@ -184,7 +285,16 @@ async function buildDirectoryTree(currentPath, assetRootPath, maxDepth = 5, curr
}
return result;
}
// Function to recognize Unity asset types based on file extension
/**
* Determines the Unity asset type based on the file extension.
*
* Extracts the file extension from the provided file path, converts it to lower case,
* and returns a matching asset type according to predefined mapping. If the extension is not recognized,
* the function returns "Other".
*
* @param {string} filePath - The file path from which the asset type is derived.
* @returns {string} The Unity asset type (e.g., "Scene", "Prefab", "Texture") or "Other" if unrecognized.
*/
function getUnityAssetType(filePath) {
const ext = path.extname(filePath).toLowerCase();
// Common Unity asset types
Expand Down Expand Up @@ -229,7 +339,24 @@ function getUnityAssetType(filePath) {
};
return assetTypes[ext] || 'Other';
}
// Handler function to process filesystem tools
/**
* Processes filesystem tool commands by validating input arguments, normalizing file paths,
* and executing the corresponding filesystem operation.
*
* This asynchronous function supports various commands such as reading files, writing files,
* editing file contents, listing directories, constructing directory trees, searching files,
* retrieving file information, finding assets by type, and listing C# scripts. It validates
* command-specific arguments using predefined schemas and ensures that file paths are confined
* within the project directory. When a command is unrecognized or arguments are invalid, it
* returns an error response.
*
* @param {string} name - Identifier of the filesystem tool command (e.g., "read_file", "write_file").
* @param {*} args - Command-specific arguments whose structure is validated with predefined schemas.
* @param {string} projectPath - Root directory used to resolve and validate file paths.
* @returns {Promise<Object>} A promise that resolves to an object containing:
* - content: An array of objects with 'type' and 'text' properties representing the response message.
* - isError: (Optional) A boolean flag indicating whether an error occurred.
*/
export async function handleFilesystemTool(name, args, projectPath) {
switch (name) {
case "read_file": {
Expand Down Expand Up @@ -392,7 +519,19 @@ export async function handleFilesystemTool(name, args, projectPath) {
const validPath = await validatePath(parsed.data.searchPath, projectPath);
const results = [];
const targetType = parsed.data.assetType.toLowerCase();
// Recursive function to search for assets
/**
* Recursively searches a directory for Unity assets matching a specific type.
*
* This asynchronous function traverses the directory tree starting at the given directory.
* For each file, it determines its Unity asset type using the external getUnityAssetType function.
* If the asset type (in lowercase) matches the externally defined targetType, the file path is added to
* the global results array.
*
* @param {string} dir - The directory path to search.
*
* @remark This function relies on external variables: targetType (a string representing the desired asset type)
* and results (an array where matching asset paths are collected).
*/
async function searchAssets(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
Expand Down Expand Up @@ -428,7 +567,17 @@ export async function handleFilesystemTool(name, args, projectPath) {
}
const validPath = await validatePath(parsed.data.path, projectPath);
const scripts = [];
// Recursive function to find C# scripts
/**
* Recursively finds all C# script files (.cs) within the specified directory.
*
* This asynchronous function traverses the given directory and its subdirectories.
* When it encounters a file with a ".cs" extension, it appends an object containing
* the file's full path and name to the global `scripts` array.
*
* @param {string} dir - The directory path to begin the search.
*
* @throws {Error} If reading the directory fails.
*/
async function findScripts(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
Expand Down Expand Up @@ -464,7 +613,13 @@ export async function handleFilesystemTool(name, args, projectPath) {
}
// Register filesystem tools with the MCP server
// This function is now only a stub that doesn't actually do anything
// since all tools are registered in toolDefinitions.ts
/**
* Deprecated function for registering filesystem tools.
*
* This function now only logs a message indicating that filesystem tool registration has moved to toolDefinitions.ts.
*
* @deprecated Filesystem tools registration is now performed in toolDefinitions.ts.
*/
export function registerFilesystemTools(server, wsHandler) {
// This function is now deprecated as tool registration has moved to toolDefinitions.ts
console.log("Filesystem tools are now registered in toolDefinitions.ts");
Expand Down
13 changes: 13 additions & 0 deletions mcpServer/build/toolDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ export const FindAssetsByTypeArgsSchema = z.object({
export const ListScriptsArgsSchema = z.object({
path: z.string().optional().default("Scripts").describe('Path to look for scripts in. Can be absolute or relative to Unity project Assets folder. If empty, defaults to the Assets/Scripts folder.'),
});
/**
* Registers available Unity Editor and filesystem tools with the MCP server and configures tool request handling.
*
* This function determines the project root from the UNITY_PROJECT_PATH environment variable (or defaults
* to the current working directory) and registers a set of tools—each defined with its name, description,
* category, tags, and input schema—for both Unity Editor operations and filesystem interactions.
* It also sets up a request handler that routes tool invocation requests based on the tool name,
* handling special cases such as connection verification, filesystem operations (via a dedicated handler),
* and Unity-specific commands. If the Unity Editor is not connected when required, it throws an McpError.
*
* @remark Tools like "verify_connection" are processed even if the Unity Editor is not connected, while other
* Unity-specific tools require an active connection and perform additional error handling.
*/
export function registerTools(server, wsHandler) {
// Determine project root path from environment variable or default to parent of Assets folder
const projectPath = process.env.UNITY_PROJECT_PATH || path.resolve(process.cwd());
Expand Down
Loading