Skip to main content
Glama

discover_projs

Scan directories to find Xcode project and workspace files for iOS/macOS development projects.

Instructions

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

Input Schema

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

Implementation Reference

  • Main handler function that orchestrates project discovery: validates input paths, checks directory existence, initiates recursive scan, formats and returns results with projects and workspaces lists.
    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,
      };
    }
  • Recursive directory scanner that finds .xcodeproj and .xcworkspace directories, skips common build dirs and symlinks, respects max depth, and ensures paths stay within workspace root.
    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'})`,
          );
        }
      }
    }
  • Registers the 'discover_projs' tool on the MCP server with input schema, description, and handler function.
    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,
            };
          }
        },
      );
    }
  • Zod input schema for the discover_projs tool parameters: workspaceRoot (string), scanPath (optional string), maxDepth (optional nonnegative int, default 5).
    {
      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
      ),
  • Orchestrator registration entry that conditionally includes the discover projects tool based on environment variable.
    {
      register: registerDiscoverProjectsTool,
      groups: [ToolGroup.PROJECT_DISCOVERY],
      envVar: 'XCODEBUILDMCP_TOOL_DISCOVER_PROJECTS',
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden. It describes a read-only scanning operation but doesn't disclose important behavioral traits: whether it's recursive by default (implied by maxDepth parameter), performance characteristics, error handling, output format, or any side effects. The description is minimal and leaves critical behavior unspecified.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core functionality. It wastes no words while covering the essential action (scan), target (directory), and outputs (specific file types). Every word earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no annotations and no output schema, the description is insufficiently complete. It doesn't explain what the tool returns (list of paths? structured data?), error conditions, or how it interacts with the many sibling build tools. For a discovery tool in a complex iOS/macOS development environment, more context about the output and integration would be valuable.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all three parameters thoroughly. The description mentions directory scanning with default workspace root, which aligns with the schema but adds no additional semantic context beyond what's in the parameter descriptions. This meets the baseline for high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: scanning a directory to find Xcode project and workspace files. It specifies the resource (directory) and verb (scan/find) with concrete file types (.xcodeproj, .xcworkspace). However, it doesn't explicitly differentiate from sibling tools, which are mostly build/run/management tools rather than discovery tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention prerequisites, typical use cases, or relationships to the many sibling build/management tools. The agent must infer usage from the tool name and description alone.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

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

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