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

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

No annotations are provided, so the description carries the full burden. It mentions scanning behavior and default values, but lacks critical details: whether it's read-only (likely, but not stated), performance implications (e.g., time for deep scans), error handling (e.g., invalid paths), or output format (just says 'find' without specifying structure). For a tool with no annotations, this leaves significant behavioral gaps.

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 action ('Scans a directory') and key details (default location, target file types). There is no wasted verbiage, and it directly addresses the tool's function without unnecessary elaboration.

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 the tool's moderate complexity (directory scanning with parameters) and lack of both annotations and output schema, the description is incomplete. It doesn't explain what the output looks like (e.g., list of paths, structured data), error conditions, or how it integrates with sibling tools (e.g., using discovered projects for building). For a discovery tool in a development context, more contextual guidance is needed.

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 fully documents all three parameters (workspaceRoot, scanPath, maxDepth) with their types, defaults, and constraints. The description adds minimal value beyond the schema—it mentions the default scan location but doesn't explain parameter interactions or provide examples. Baseline 3 is appropriate when the schema does the heavy lifting.

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 specific file types (.xcodeproj and .xcworkspace). It specifies the verb 'scans' and resource 'directory', and mentions the default location. However, it doesn't explicitly differentiate from sibling tools like 'discover_tools' or explain why this specific discovery is needed versus general tool discovery.

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 (e.g., needing Xcode installed), compare it to sibling tools like 'discover_tools' or project-building tools, or specify scenarios where this discovery is necessary (e.g., before building or testing). Usage is implied but not explicitly stated.

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

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