Skip to main content
Glama
discover_projects.ts9.48 kB
/** * Project Discovery Tools - Find Xcode projects and workspaces * * This module provides tools for scanning directories to discover Xcode project (.xcodeproj) * and workspace (.xcworkspace) files. This is useful for initial project exploration and * for identifying available projects to work with. * * Responsibilities: * - Recursively scanning directories for Xcode projects and workspaces * - Filtering out common directories that should be skipped (build, DerivedData, etc.) * - Respecting maximum depth limits to prevent excessive scanning * - Providing formatted output with absolute paths for discovered files */ import { z } from 'zod'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { log } from '../utils/logger.js'; import { ToolResponse } from '../types/common.js'; import path from 'node:path'; import fs from 'node:fs/promises'; import { createTextContent } from './common.js'; // Constants const DEFAULT_MAX_DEPTH = 5; const SKIPPED_DIRS = new Set(['build', 'DerivedData', 'Pods', '.git', 'node_modules']); // Type definition for parameters type DiscoverProjectsParams = { scanPath?: string; maxDepth: number; workspaceRoot: string; }; // --- Private Helper Function --- /** * Recursively scans directories to find Xcode projects and workspaces. */ async function _findProjectsRecursive( currentDirAbs: string, workspaceRootAbs: string, currentDepth: number, maxDepth: number, results: { projects: string[]; workspaces: string[] }, ): Promise<void> { // Explicit depth check (now simplified as maxDepth is always non-negative) if (currentDepth >= maxDepth) { log('debug', `Max depth ${maxDepth} reached at ${currentDirAbs}, stopping recursion.`); return; } log('debug', `Scanning directory: ${currentDirAbs} at depth ${currentDepth}`); const normalizedWorkspaceRoot = path.normalize(workspaceRootAbs); try { const entries = await fs.readdir(currentDirAbs, { withFileTypes: true }); for (const entry of entries) { const absoluteEntryPath = path.join(currentDirAbs, entry.name); const relativePath = path.relative(workspaceRootAbs, absoluteEntryPath); // --- Skip conditions --- if (entry.isSymbolicLink()) { log('debug', `Skipping symbolic link: ${relativePath}`); continue; } // Skip common build/dependency directories by name if (entry.isDirectory() && SKIPPED_DIRS.has(entry.name)) { log('debug', `Skipping standard directory: ${relativePath}`); continue; } // Ensure entry is within the workspace root (security/sanity check) if (!path.normalize(absoluteEntryPath).startsWith(normalizedWorkspaceRoot)) { log( 'warn', `Skipping entry outside workspace root: ${absoluteEntryPath} (Workspace: ${workspaceRootAbs})`, ); continue; } // --- Process entries --- if (entry.isDirectory()) { let isXcodeBundle = false; if (entry.name.endsWith('.xcodeproj')) { results.projects.push(absoluteEntryPath); // Use absolute path log('debug', `Found project: ${absoluteEntryPath}`); isXcodeBundle = true; } else if (entry.name.endsWith('.xcworkspace')) { results.workspaces.push(absoluteEntryPath); // Use absolute path log('debug', `Found workspace: ${absoluteEntryPath}`); isXcodeBundle = true; } // Recurse into regular directories, but not into found project/workspace bundles if (!isXcodeBundle) { await _findProjectsRecursive( absoluteEntryPath, workspaceRootAbs, currentDepth + 1, maxDepth, results, ); } } } } catch (error: unknown) { let code: string | undefined; let message = 'Unknown error'; if (error instanceof Error) { message = error.message; if ('code' in error) { code = (error as NodeJS.ErrnoException).code; } } else if (typeof error === 'object' && error !== null) { if ('message' in error && typeof error.message === 'string') { message = error.message; } if ('code' in error && typeof error.code === 'string') { code = error.code; } } else { message = String(error); } if (code === 'EPERM' || code === 'EACCES') { log('debug', `Permission denied scanning directory: ${currentDirAbs}`); } else { log( 'warning', `Error scanning directory ${currentDirAbs}: ${message} (Code: ${code ?? 'N/A'})`, ); } } } /** * Internal logic for discovering projects. */ async function _handleDiscoveryLogic(params: DiscoverProjectsParams): Promise<ToolResponse> { const { scanPath: relativeScanPath, maxDepth, workspaceRoot } = params; // Calculate and validate the absolute scan path const requestedScanPath = path.resolve(workspaceRoot, relativeScanPath || '.'); let absoluteScanPath = requestedScanPath; const normalizedWorkspaceRoot = path.normalize(workspaceRoot); if (!path.normalize(absoluteScanPath).startsWith(normalizedWorkspaceRoot)) { log( 'warn', `Requested scan path '${relativeScanPath}' resolved outside workspace root '${workspaceRoot}'. Defaulting scan to workspace root.`, ); absoluteScanPath = normalizedWorkspaceRoot; } const results = { projects: [] as string[], workspaces: [] as string[] }; log( 'info', `Starting project discovery request: path=${absoluteScanPath}, maxDepth=${maxDepth}, workspace=${workspaceRoot}`, ); try { // Ensure the scan path exists and is a directory const stats = await fs.stat(absoluteScanPath); if (!stats.isDirectory()) { const errorMsg = `Scan path is not a directory: ${absoluteScanPath}`; log('error', errorMsg); // Return ToolResponse error format return { content: [createTextContent(errorMsg)], isError: true, }; } } catch (error: unknown) { let code: string | undefined; let message = 'Unknown error accessing scan path'; // Type guards - refined if (error instanceof Error) { message = error.message; // Check for code property specific to Node.js fs errors if ('code' in error) { code = (error as NodeJS.ErrnoException).code; } } else if (typeof error === 'object' && error !== null) { if ('message' in error && typeof error.message === 'string') { message = error.message; } if ('code' in error && typeof error.code === 'string') { code = error.code; } } else { message = String(error); } const errorMsg = `Failed to access scan path: ${absoluteScanPath}. Error: ${message}`; log('error', `${errorMsg} - Code: ${code ?? 'N/A'}`); return { content: [createTextContent(errorMsg)], isError: true, }; } // Start the recursive scan from the validated absolute path await _findProjectsRecursive(absoluteScanPath, workspaceRoot, 0, maxDepth, results); log( 'info', `Discovery finished. Found ${results.projects.length} projects and ${results.workspaces.length} workspaces.`, ); const responseContent = [ createTextContent( `Discovery finished. Found ${results.projects.length} projects and ${results.workspaces.length} workspaces.`, ), ]; // Sort results for consistent output results.projects.sort(); results.workspaces.sort(); if (results.projects.length > 0) { responseContent.push( createTextContent(`Projects found:\n - ${results.projects.join('\n - ')}`), ); } if (results.workspaces.length > 0) { responseContent.push( createTextContent(`Workspaces found:\n - ${results.workspaces.join('\n - ')}`), ); } return { content: responseContent, projects: results.projects, workspaces: results.workspaces, isError: false, }; } // --- Public Tool Definition --- export function registerDiscoverProjectsTool(server: McpServer): void { server.tool( 'discover_projs', 'Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.', { workspaceRoot: z.string().describe('The absolute path of the workspace root to scan within.'), scanPath: z .string() .optional() .describe('Optional: Path relative to workspace root to scan. Defaults to workspace root.'), maxDepth: z.number().int().nonnegative().optional().default(DEFAULT_MAX_DEPTH).describe( `Optional: Maximum directory depth to scan. Defaults to ${DEFAULT_MAX_DEPTH}.`, // Removed mention of -1 ), }, async (params) => { try { return await _handleDiscoveryLogic(params as DiscoverProjectsParams); } catch (error: unknown) { let errorMessage = ''; if (error instanceof Error) { errorMessage = `An unexpected error occurred during project discovery: ${error.message}`; log('error', `${errorMessage}\n${error.stack ?? ''}`); } else { const errorString = String(error); log('error', `Caught non-Error value during project discovery: ${errorString}`); errorMessage = `An unexpected non-error value was thrown: ${errorString}`; } return { content: [createTextContent(errorMessage)], isError: true, }; } }, ); }

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/SampsonKY/XcodeBuildMCP'

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