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.`);
    }

Tool Definition Quality

Score is being calculated. Check back soon.

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

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