Skip to main content
Glama

discover_projs

Scan directories to locate Xcode project (.xcodeproj) and workspace (.xcworkspace) files, specifying workspace root, scan path, and depth for efficient discovery.

Instructions

Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
maxDepthNoOptional: Maximum directory depth to scan. Defaults to 5.
scanPathNoOptional: Path relative to workspace root to scan. Defaults to workspace root.
workspaceRootYesThe absolute path of the workspace root to scan within.

Implementation Reference

  • Core execution logic for the discover_projs tool: validates params, scans directories recursively for .xcodeproj and .xcworkspace files, handles errors, and formats response.
    export async function discover_projsLogic(
      params: DiscoverProjsParams,
      fileSystemExecutor: FileSystemExecutor,
    ): Promise<ToolResponse> {
      // Apply defaults
      const scanPath = params.scanPath ?? '.';
      const maxDepth = params.maxDepth ?? DEFAULT_MAX_DEPTH;
      const workspaceRoot = params.workspaceRoot;
    
      const relativeScanPath = scanPath;
    
      // 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: [], workspaces: [] };
    
      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 fileSystemExecutor.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) {
        let code;
        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.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,
        fileSystemExecutor,
      );
    
      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,
        isError: false,
      };
    }
  • Zod schema defining input parameters for the discover_projs tool: workspaceRoot (required string), scanPath (optional string), maxDepth (optional nonnegative int).
    const discoverProjsSchema = z.object({
      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()
        .describe(`Optional: Maximum directory depth to scan. Defaults to ${DEFAULT_MAX_DEPTH}.`),
    });
  • Default export defining the tool for plugin loading: includes name, description, schema, and handler wrapper using createTypedTool that invokes discover_projsLogic.
    export default {
      name: 'discover_projs',
      description:
        'Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.',
      schema: discoverProjsSchema.shape, // MCP SDK compatibility
      handler: createTypedTool(
        discoverProjsSchema,
        (params: DiscoverProjsParams) => {
          return discover_projsLogic(params, getDefaultFileSystemExecutor());
        },
        getDefaultCommandExecutor,
      ),
    };
  • Recursive helper function that scans directories for Xcode projects and workspaces, skipping symlinks, build dirs, and respecting max depth and workspace boundaries.
    async function _findProjectsRecursive(
      currentDirAbs: string,
      workspaceRootAbs: string,
      currentDepth: number,
      maxDepth: number,
      results: { projects: string[]; workspaces: string[] },
      fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
    ): 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 {
        // Use the injected fileSystemExecutor
        const entries = await fileSystemExecutor.readdir(currentDirAbs, { withFileTypes: true });
        for (const rawEntry of entries) {
          // Cast the unknown entry to DirentLike interface for type safety
          const entry = rawEntry as DirentLike;
          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,
                fileSystemExecutor,
              );
            }
          }
        }
      } catch (error) {
        let code;
        let message = 'Unknown error';
    
        if (error instanceof Error) {
          message = error.message;
          if ('code' in error) {
            code = error.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'})`,
          );
        }
      }
    }
  • Dynamic registration function that loads and registers discovery tools including discover_projs from plugin registry.
    export async function registerDiscoveryTools(server: McpServer): Promise<void> {
      const plugins = await loadPlugins();
      let registeredCount = 0;
    
      // Only register discovery tools initially
      const discoveryTools = [];
      for (const plugin of plugins.values()) {
        // Only load discover_tools and discover_projs initially - other tools will be loaded via workflows
        if (plugin.name === 'discover_tools' || plugin.name === 'discover_projs') {
          discoveryTools.push({
            name: plugin.name,
            config: {
              description: plugin.description ?? '',
              inputSchema: plugin.schema,
            },
            // Adapt callback to match SDK's expected signature
            callback: (args: unknown): Promise<ToolResponse> =>
              plugin.handler(args as Record<string, unknown>),
          });
          registeredCount++;
        }
      }
    
      // Register discovery tools using bulk registration with tracking
      if (discoveryTools.length > 0) {
        registerAndTrackTools(server, discoveryTools);
      }
    
      log('info', `✅ Registered ${registeredCount} discovery tools in dynamic mode.`);
    }
Install Server

Other Tools

Related Tools

Latest Blog Posts

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

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