Skip to main content
Glama
smat-dev

Jinni: Bring Your Project Into Context

by smat-dev

read_context

Extract and analyze project context by focusing on specified files or directories within a root path. Provides a static view of relevant files, using default exclusions or custom rules for precise filtering.

Instructions

Reads context from a specified project root directory (absolute path). Focuses on the specified target files/directories within that root. Returns a static view of files with paths relative to the project root. Assume the user wants to read in context for the whole project unless otherwise specified - do not ask the user for clarification if just asked to read context. If the user just says 'jinni', interpret that as read_context. If the user asks to list context, use the list_only argument. Both targets and rules accept a JSON array of strings. The project_root, targets, and rules arguments are mandatory. You can ignore the other arguments by default. IMPORTANT NOTE ON RULES: Ensure you understand the rule syntax (details available via the usage tool) before providing specific rules. Using rules=[] is recommended if unsure, as this uses sensible defaults.

Guidance for AI Model Usage

When requesting context using this tool:

  • Default Behavior: If you provide an empty rules list ([]), Jinni uses sensible default exclusions (like .git, node_modules, __pycache__, common binary types) combined with any project-specific .contextfiles. This usually provides the "canonical context" - files developers typically track in version control. Assume this is what the users wants if they just ask to read context.

  • Targeting Specific Files: If you have a list of specific files you need (e.g., ["src/main.py", "README.md"]), provide them in the targets list. This is efficient and precise, quicker than reading one by one.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
debug_explainNo
exclusionsNoOptional exclusion configuration. Object with 'global' (list of keywords), 'scoped' (object mapping paths to keyword lists), and 'patterns' (list of file patterns) fields.
list_onlyNo
project_rootYes**MUST BE ABSOLUTE PATH**. The absolute path to the project root directory.
rulesYes**Mandatory**. List of inline filtering rules. Provide `[]` if no specific rules are needed (uses defaults). It is strongly recommended to consult the `usage` tool documentation before providing a non-empty list.
size_limit_mbNo
targetsYes**Mandatory**. List of paths (absolute or relative to CWD) to specific files or directories within the project root to process. Must be a JSON array of strings. If empty (`[]`), the entire `project_root` is processed.

Implementation Reference

  • MCP tool handler for 'read_context'. This async function defines the tool logic, input validation using Pydantic Fields (schema), path translation, exclusion handling, and delegates to core_read_context from core_logic.py. Registered via @server.tool decorator.
    @server.tool(description=(
        "Reads context from a specified project root directory (absolute path). "
        "Focuses on the specified target files/directories within that root. "
        "Returns a static view of files with paths relative to the project root. "
        "Assume the user wants to read in context for the whole project unless otherwise specified - "
        "do not ask the user for clarification if just asked to read context. "
        "If the user just says 'jinni', interpret that as read_context. "
        "If the user asks to list context, use the list_only argument. "
        "Both `targets` and `rules` accept a JSON array of strings. "
        "The `project_root`, `targets`, and `rules` arguments are mandatory. "
        "You can ignore the other arguments by default. "
        "IMPORTANT NOTE ON RULES: Ensure you understand the rule syntax (details available via the `usage` tool) before providing specific rules. "
        "Using `rules=[]` is recommended if unsure, as this uses sensible defaults.\n\n"
        "**Guidance for AI Model Usage**\n\n"
        "When requesting context using this tool:\n"
        "*   **Default Behavior:** If you provide an empty `rules` list (`[]`), Jinni uses sensible default exclusions (like `.git`, `node_modules`, `__pycache__`, common binary types) combined with any project-specific `.contextfiles`. This usually provides the \"canonical context\" - files developers typically track in version control. Assume this is what the users wants if they just ask to read context.\n"
        "*   **Targeting Specific Files:** If you have a list of specific files you need (e.g., `[\"src/main.py\", \"README.md\"]`), provide them in the `targets` list. This is efficient and precise, quicker than reading one by one.\n"
    ))
    
    
    
    
    async def read_context(
        project_root: str = Field(description="**MUST BE ABSOLUTE PATH**. The absolute path to the project root directory."),
        targets: List[str] = Field(description="**Mandatory**. List of paths (absolute or relative to CWD) to specific files or directories within the project root to process. Must be a JSON array of strings. If empty (`[]`), the entire `project_root` is processed."),
        rules: List[str] = Field(description="**Mandatory**. List of inline filtering rules. Provide `[]` if no specific rules are needed (uses defaults). It is strongly recommended to consult the `usage` tool documentation before providing a non-empty list."),
        list_only: bool = False,
        size_limit_mb: Optional[int] = None,
        debug_explain: bool = False,
        exclusions: Optional[dict] = Field(default=None, description="Optional exclusion configuration. Object with 'global' (list of keywords), 'scoped' (object mapping paths to keyword lists), and 'patterns' (list of file patterns) fields."),
    ) -> str:
        logger.info("--- read_context tool invoked ---")
        # Translate incoming paths *before* any validation or Path object creation
        translated_project_root = _translate_wsl_path(project_root)
        translated_targets = [_translate_wsl_path(t) for t in targets]
        logger.debug(f"Original paths: project_root='{project_root}', targets='{targets}'")
        logger.debug(f"Translated paths: project_root='{translated_project_root}', targets='{translated_targets}'")
    
        # Defensive NUL check on all incoming paths
        ensure_no_nul(translated_project_root, "project_root")
        for t in translated_targets:
            ensure_no_nul(t, "target path")
    
        logger.debug(f"Processing read_context request: project_root(orig)='{project_root}', targets(orig)='{targets}', list_only={list_only}, rules={rules}, debug_explain={debug_explain}")
        """
        Generates a concatenated view of relevant code files for a given target path.
    
        The 'project_root' argument must always be an absolute path.
        The optional 'targets' argument, if provided, must be a list of paths (JSON array of strings).
        Each path must be absolute or relative to the current working directory, and must resolve to a location
        *inside* the 'project_root'.
    
        If the server was started with a --root argument, the provided 'project_root' must be
        within that server root directory.
        
        Args:
            project_root: See Field description.
            targets: See Field description.
            rules: See Field description.
            list_only: Only list file paths found. Defaults to False.
            size_limit_mb: Override the maximum total context size in MB. Defaults to None (uses core_logic default).
            debug_explain: Print detailed explanation for file/directory inclusion/exclusion to server's stderr. Defaults to False.
        """
        # --- Input Validation ---
        # Use the translated project_root for validation
        if not os.path.isabs(translated_project_root):
             raise ValueError(f"Tool 'project_root' argument must be absolute (after translation), received: '{translated_project_root}' from original '{project_root}'")
        resolved_project_root_path = Path(translated_project_root).resolve()
        if not resolved_project_root_path.is_dir():
             raise ValueError(f"Tool 'project_root' path does not exist or is not a directory: {resolved_project_root_path} (translated from '{project_root}')")
        resolved_project_root_path_str = str(resolved_project_root_path) # Store translated path as string
        logger.debug(f"Using project_root (translated): {resolved_project_root_path_str}")
    
        # Validate mandatory targets list (can be empty)
        # No need for `is None` check, Pydantic/FastMCP ensures it's a list.
    
        resolved_target_paths_str: List[str] = []
        effective_targets_set: Set[str] = set() # Use set to handle duplicates implicitly
    
        # Process the provided targets list if it's not empty
        if translated_targets:
            logger.debug(f"Processing translated targets list: {translated_targets}")
            for idx, single_target in enumerate(translated_targets):
                if not isinstance(single_target, str):
                     raise TypeError(f"Tool 'targets' item at index {idx} must be a string, got {type(single_target)}")
    
                # Check if target is absolute. If not, resolve relative to project_root.
                target_path_obj = Path(single_target)
                if target_path_obj.is_absolute():
                    resolved_target_path = target_path_obj.resolve()
                else:
                    # Resolve relative path against the project root
                    resolved_target_path = (resolved_project_root_path / target_path_obj).resolve()
                    logger.debug(f"Resolved relative target '{single_target}' to '{resolved_target_path}' using project root '{resolved_project_root_path}'")
                if not resolved_target_path.exists():
                     raise FileNotFoundError(f"Tool 'targets' path '{single_target}' (resolved to {resolved_target_path}) does not exist.")
                # Check if target is within project_root AFTER resolving
                try:
                    resolved_target_path.relative_to(resolved_project_root_path)
                except ValueError:
                     raise ValueError(f"Tool 'targets' path '{resolved_target_path}' is outside the specified project root '{resolved_project_root_path}'")
    
                resolved_path_str = str(resolved_target_path)
                if resolved_path_str not in effective_targets_set:
                     resolved_target_paths_str.append(resolved_path_str)
                     effective_targets_set.add(resolved_path_str)
                     logger.debug(f"Validated target path from targets[{idx}]: {resolved_path_str}")
                else:
                     logger.debug(f"Skipping duplicate target path from targets[{idx}]: {resolved_path_str}")
    
        # If the initial targets list was empty OR it resulted in an empty list after validation,
        # default to processing the project root.
        if not resolved_target_paths_str:
            logger.debug("Targets list is empty or resulted in no valid paths. Defaulting to project root.")
            resolved_target_paths_str = [resolved_project_root_path_str]
    
        # Validate mandatory rules list (can be empty, but must be provided)
        if rules is None: # Should not happen if Pydantic enforces mandatory, but good practice
            raise ValueError("Tool 'rules' argument is mandatory. Provide an empty list [] if no specific rules are needed.")
        if not isinstance(rules, list):
            raise TypeError(f"Tool 'rules' argument must be a list, got {type(rules)}")
        for idx, rule in enumerate(rules):
            if not isinstance(rule, str):
                raise TypeError(f"Tool 'rules' item at index {idx} must be a string, got {type(rule)}")
        logger.debug(f"Using provided rules: {rules}")
    
    
        # --- Validate against Server Root (if set) ---
        # The *project_root* provided by the client must be within the server's root (if set)
        if SERVER_ROOT_PATH:
            logger.debug(f"Server root is set: {SERVER_ROOT_PATH}")
            try:
                resolved_project_root_path.relative_to(SERVER_ROOT_PATH)
                logger.debug(f"Client project_root {resolved_project_root_path} is within server root {SERVER_ROOT_PATH}")
            except ValueError:
                 raise ValueError(f"Tool project_root '{resolved_project_root_path}' is outside the allowed server root '{SERVER_ROOT_PATH}'")
    
        # --- Process Exclusions ---
        exclusion_parser = None
        exclusion_patterns = []
        if exclusions:
            from jinni.exclusion_parser import ExclusionParser
            
            # Extract exclusion components
            global_keywords = exclusions.get('global', [])
            scoped_exclusions = exclusions.get('scoped', {})
            file_patterns = exclusions.get('patterns', [])
            
            # Convert scoped dict to list format expected by ExclusionParser
            scoped_list = []
            for path, keywords in scoped_exclusions.items():
                if isinstance(keywords, list):
                    scoped_list.append(f"{path}:{','.join(keywords)}")
            
            # Create exclusion parser
            parser = ExclusionParser()
            
            # Parse different exclusion types
            exclusion_patterns.extend(parser.parse_not(global_keywords))
            exclusion_patterns.extend(parser.parse_not_in(scoped_list))
            exclusion_patterns.extend(parser.parse_not_files(file_patterns))
            
            if exclusion_patterns:
                exclusion_parser = parser
                logger.info(f"Configured {len(exclusion_patterns)} exclusion patterns")
    
        logger.info(f"Processing project_root: {resolved_project_root_path_str}")
        # Log the final list of targets being processed
        # Log the final list of targets being processed
        # Log the final list of targets being processed
        logger.info(f"Focusing on target(s): {resolved_target_paths_str}")
        # --- Call Core Logic ---
        log_capture_buffer = None
        temp_handler = None
        loggers_to_capture = []
        debug_output = ""
    
        try:
            if debug_explain:
                # Setup temporary handler to capture debug logs
                log_capture_buffer = io.StringIO()
                temp_handler = logging.StreamHandler(log_capture_buffer)
                temp_handler.setLevel(logging.DEBUG)
                # Simple formatter for captured logs
                formatter = logging.Formatter('%(name)s:%(levelname)s: %(message)s')
                temp_handler.setFormatter(formatter)
    
                # Add handler to relevant core logic loggers
                loggers_to_capture = [
                    logging.getLogger(name) for name in
                    ["jinni.core_logic", "jinni.context_walker", "jinni.file_processor", "jinni.config_system", "jinni.utils"]
                ]
                for core_logger in loggers_to_capture:
                    # Explicitly set level to DEBUG *before* adding handler
                    # This ensures messages are generated for the handler to capture
                    original_level = core_logger.level
                    core_logger.setLevel(logging.DEBUG)
                    core_logger.addHandler(temp_handler)
                    # Store original level? Not strictly necessary for this hack,
                    # as we remove the handler later, but good practice if we were restoring level.
    
    
            # Pass the validated list of target paths (or the project root if no target was given)
            # The variable resolved_target_paths_str already holds the correct list.
            effective_target_paths_str = resolved_target_paths_str
            
            # Combine rules with exclusion patterns
            effective_rules = rules.copy() if rules else []
            if exclusion_patterns:
                effective_rules.extend(exclusion_patterns)
            
            # Call the core logic function
            result_content = core_read_context(
                target_paths_str=effective_target_paths_str,
                project_root_str=resolved_project_root_path_str, # Pass the translated, validated root
                override_rules=effective_rules,
                list_only=list_only,
                size_limit_mb=size_limit_mb,
                debug_explain=debug_explain, # Pass flag down
                # include_size_in_list is False by default in core_logic if not passed
                exclusion_parser=exclusion_parser # Pass exclusion parser for scoped exclusions
            )
            logger.debug(f"Finished processing project_root: {resolved_project_root_path_str}, targets(s): {resolved_target_paths_str}. Result length: {len(result_content)}")
    
            if debug_explain and log_capture_buffer:
                debug_output = log_capture_buffer.getvalue()
    
            # Combine result and debug output if necessary
            if debug_output:
                return f"{result_content}\n\n--- DEBUG LOG ---\n{debug_output}"
            else:
                return result_content
    
        except (FileNotFoundError, ContextSizeExceededError, ValueError, DetailedContextSizeError) as e:
            # Let FastMCP handle converting these known errors
            logger.error(f"Error during read_context call for project_root='{resolved_project_root_path_str}', targets(s)='{resolved_target_paths_str}': {type(e).__name__} - {e}")
            raise e # Re-raise for FastMCP
        except Exception as e:
            # Log unexpected errors before FastMCP potentially converts to a generic 500
            logger.exception(f"Unexpected error processing project_root='{resolved_project_root_path_str}', targets(s)='{resolved_target_paths_str}': {type(e).__name__} - {e}")
            raise e
        finally:
            # --- Cleanup: Remove temporary handler ---
            if temp_handler and loggers_to_capture:
                logger.debug("Removing temporary debug log handler.")
                for core_logger in loggers_to_capture:
                    core_logger.removeHandler(temp_handler)
                temp_handler.close()
  • Core implementation logic delegated by the MCP handler. Orchestrates context processing: input validation, root determination, size limits, override rules, directory walking, file processing, and output formatting.
    def read_context(
        target_paths_str: List[str], # List of targets from CLI or constructed by Server
        project_root_str: Optional[str] = None, # Optional from CLI, Mandatory from Server (used as base)
        override_rules: Optional[List[str]] = None,
        list_only: bool = False,
        size_limit_mb: Optional[int] = None,
        debug_explain: bool = False,
        include_size_in_list: bool = False,
        exclusion_parser: Optional[Any] = None  # ExclusionParser instance for scoped exclusions
    ) -> str:
        """
        Orchestrates the context reading process, handling flexible inputs.
    
        Validates inputs, determines the effective roots for rule discovery and output,
        resolves targets, and delegates processing to file_processor or context_walker.
    
        Args:
            target_paths_str: List of target file/directory paths (relative or absolute).
            project_root_str: Optional path to the project root. If provided, it's used as the
                              base for rule discovery and output relativity. If None, it's
                              inferred from the common ancestor of targets.
            override_rules: Optional list of rule strings to use instead of .contextfiles.
            list_only: If True, only return a list of relative file paths.
            size_limit_mb: Optional override for the size limit in MB.
            debug_explain: If True, log inclusion/exclusion reasons.
            include_size_in_list: If True and list_only, prepend size to path.
    
        Returns:
            A formatted string (concatenated content or file list).
    
        Raises:
            FileNotFoundError: If any target path does not exist.
            ValueError: If paths have issues (e.g., target outside explicit root).
            DetailedContextSizeError: If context size limit is exceeded.
            ImportError: If pathspec is required but not installed.
        """
        # --- Initial Setup & Validation ---
    
        # Validate project_root_str FIRST if provided, and set roots
        output_rel_root: Path
        rule_discovery_root: Path
        project_root_path: Optional[Path] = None # Store resolved explicit project_root
    
        if project_root_str:
            project_root_path = Path(project_root_str).resolve()
            if not project_root_path.is_dir():
                # Raise ValueError immediately if explicit root is invalid
                raise ValueError(f"Provided project root '{project_root_str}' does not exist or is not a directory.")
            output_rel_root = project_root_path
            rule_discovery_root = project_root_path
            logger.debug(f"Using provided project root for output relativity and rule discovery boundary: {output_rel_root}")
        # else: Roots will be determined after resolving targets
    
        # Resolve target paths (relative to CWD by default)
        target_paths: List[Path] = []
        if not target_paths_str:
             # Handle case where CLI provides no paths (defaults to ['.'])
             # or Server provides no target (meaning process root)
             if project_root_str:
                  # If root is given but no targets, process the root
                  target_paths_str = [project_root_str]
                  logger.debug("No specific targets provided; processing project root.")
             else:
                  # If no root and no targets, default to current dir '.'
                  target_paths_str = ['.']
                  logger.debug("No specific targets or project root provided; processing current directory '.'")
    
        for p_str in target_paths_str:
            p = Path(p_str).resolve() # Resolve paths here to ensure they are absolute
            if not p.exists():
                raise FileNotFoundError(f"Target path does not exist: {p_str} (resolved to {p})")
            target_paths.append(p)
    
        if not target_paths:
            logger.warning("No valid target paths could be determined.")
            return ""
    
        # Determine roots IF project_root wasn't provided explicitly
        if not project_root_path:
            try:
                common_ancestor = Path(os.path.commonpath([str(p) for p in target_paths]))
                calculated_root = common_ancestor if common_ancestor.is_dir() else common_ancestor.parent
            except ValueError:
                logger.warning("Could not find common ancestor for targets. Using CWD as root.")
                calculated_root = Path.cwd().resolve()
            output_rel_root = calculated_root
            rule_discovery_root = calculated_root
            logger.debug(f"Using common ancestor/CWD as output relativity and rule discovery boundary root: {output_rel_root}")
        # else: Roots were already set from the valid project_root_path
    
        # Ensure roots are set (safeguard)
        if 'output_rel_root' not in locals() or 'rule_discovery_root' not in locals():
             logger.error("Critical error: Output/Rule discovery root could not be determined.")
             raise ValueError("Could not determine a root directory.")
    
        # Validate targets are within explicit root (if provided) AFTER resolving roots
        if project_root_path:
            for tp in target_paths:
                try:
                    # Use is_relative_to for Python 3.9+ or fallback
                    if sys.version_info >= (3, 9):
                        if not tp.is_relative_to(project_root_path):
                            raise ValueError(f"Target path {tp} is outside the specified project root {project_root_path}")
                    else:
                        tp.relative_to(project_root_path) # Check raises ValueError if not relative
                except ValueError:
                    raise ValueError(f"Target path {tp} is outside the specified project root {project_root_path}")
    
        # (Logic moved above)
    
        # --- Size Limit (Moved up slightly, no functional change) ---
        limit_mb_str = os.environ.get(ENV_VAR_SIZE_LIMIT)
        try:
            effective_limit_mb = size_limit_mb if size_limit_mb is not None \
                                 else int(limit_mb_str) if limit_mb_str else DEFAULT_SIZE_LIMIT_MB
        except ValueError:
            logger.warning(f"Invalid value for {ENV_VAR_SIZE_LIMIT} ('{limit_mb_str}'). Using default {DEFAULT_SIZE_LIMIT_MB}MB.")
            effective_limit_mb = DEFAULT_SIZE_LIMIT_MB
        size_limit_bytes = effective_limit_mb * 1024 * 1024
        logger.debug(f"Effective size limit: {effective_limit_mb}MB ({size_limit_bytes} bytes)")
    
        # --- Override Handling ---
        # Override rules are used only if the list is provided AND non-empty
        use_overrides = bool(override_rules) # bool([]) is False, bool(['rule']) is True
        override_spec: Optional['pathspec.PathSpec'] = None
        if use_overrides:
            if pathspec is None:
                 raise ImportError("pathspec library is required for override rules but not installed.")
            logger.info("Override rules provided as high-priority additions to normal rules.")
            # Store the override rules for later use in the walker
            override_spec = compile_spec_from_rules(override_rules, "Overrides")
            if debug_explain: logger.debug(f"Compiled override spec with {len(override_spec.patterns)} patterns.")
    
        # --- Processing State ---
        output_parts: List[str] = []
        processed_files_set: Set[Path] = set()
        total_size_bytes: int = 0
        # Use the resolved target_paths as the initial set for "always include" logic within walker/processor
        initial_target_paths_set: Set[Path] = set(target_paths)
    
        # --- Compute a root for each target ---
        roots_for_target: Dict[Path, Path] = {}
        
        # Determine the base for comparison (project root or CWD)
        comparison_base = project_root_path if project_root_path else Path.cwd().resolve()
        
        for tp in target_paths:
            try:
                # Check if target is within the comparison base
                if tp.is_relative_to(comparison_base):
                    root = comparison_base  # Use project root (or CWD) for targets within it
                else:
                    root = tp if tp.is_dir() else tp.parent  # External targets are self-contained
            except AttributeError:  # Python < 3.9 fallback
                if str(tp).startswith(str(comparison_base)):
                    root = comparison_base
                else:
                    root = tp if tp.is_dir() else tp.parent
            roots_for_target[tp] = root
            if debug_explain: logger.debug(f"Target {tp} will use rule root: {root}")
    
        # --- Delegate Processing ---
        try:
            # Group targets by their root and walk once per root
            for root in set(roots_for_target.values()):
                # Collect targets that belong to this root
                grouped = [t for t, r in roots_for_target.items() if r == root]
                
                # Build an "always include" set for this root
                initial_set = set(grouped)
                
                for current_target_path in grouped:
                    # Skip if already processed (e.g., listed twice or handled by a previous dir walk)
                    if current_target_path in processed_files_set:
                         if debug_explain: logger.debug(f"Skipping target {current_target_path} as it was already processed.")
                         continue
    
                    if current_target_path.is_file():
                        if debug_explain: logger.debug(f"Processing file target: {current_target_path}")
                        file_output, file_size_added = process_file(
                            file_path=current_target_path,
                            output_rel_root=output_rel_root,  # Use the original output root
                            size_limit_bytes=size_limit_bytes,
                            total_size_bytes=total_size_bytes,
                            list_only=list_only,
                            include_size_in_list=include_size_in_list,
                            debug_explain=debug_explain
                        )
                        if file_output is not None:
                            # Check if adding this file *content* exceeds limit (only if not list_only)
                            if not list_only and (total_size_bytes + file_size_added > size_limit_bytes):
                                 # Check if file alone exceeds limit
                                 if file_size_added > size_limit_bytes and total_size_bytes == 0:
                                      logger.warning(f"File {current_target_path} ({file_size_added} bytes) content exceeds size limit of {effective_limit_mb}MB. Skipping.")
                                      continue # Skip this file
                                 else:
                                      # Adding this file pushes over the limit
                                      raise ContextSizeExceededError(effective_limit_mb, total_size_bytes + file_size_added, current_target_path)
    
                            output_parts.append(file_output)
                            processed_files_set.add(current_target_path)
                            total_size_bytes += file_size_added # Add size only if content included
    
                    elif current_target_path.is_dir():
                        if debug_explain: logger.debug(f"Processing directory target: {current_target_path}")
                        dir_output_parts, dir_total_size, dir_processed_files = walk_and_process(
                            walk_target_path=current_target_path,
                            rule_root=root,  # Pass the rule root for this target
                            output_rel_root=output_rel_root, # Keep original output root for consistent paths
                            initial_target_paths_set=initial_set, # Pass initial targets for this root
                            use_overrides=use_overrides,
                            override_spec=override_spec,
                            size_limit_bytes=size_limit_bytes - total_size_bytes, # Pass remaining budget
                            list_only=list_only,
                            include_size_in_list=include_size_in_list,
                            debug_explain=debug_explain,
                            exclusion_parser=exclusion_parser # Pass exclusion parser for scoped exclusions
                        )
                        output_parts.extend(dir_output_parts)
                        processed_files_set.update(dir_processed_files)
                        total_size_bytes += dir_total_size # Accumulate size from walker
                    else:
                         logger.warning(f"Target path is neither a file nor a directory: {current_target_path}")
    
            # --- Final Output Formatting ---
            final_output = "\n".join(output_parts) if list_only else SEPARATOR.join(output_parts)
            logger.info(f"Processed {len(processed_files_set)} files, total size: {total_size_bytes} bytes.")
            return final_output
    
        except ContextSizeExceededError as e:
                # Catch error, format with large files list, and raise Detailed error
                logger.error(f"Context size limit exceeded: {e}")
                # Use output_rel_root as the base for finding large files
                large_files = get_large_files(str(output_rel_root))
                error_message = (
                    f"Error: Context size limit of {e.limit_mb}MB exceeded.\n"
                    f"Processing stopped near file: {e.file_path}\n\n"
                    "Consider excluding large files or directories using a `.contextfiles` file.\n"
                    "Consult README.md (or use `jinni doc`) for more details on exclusion rules.\n\n"
                    "Potential large files found (relative to project root):\n"
                )
                if large_files:
                    for fname, fsize in large_files:
                        size_mb = fsize / (1024 * 1024)
                        error_message += f" - {fname} ({size_mb:.2f} MB)\n"
                else:
                    error_message += " - Could not identify specific large files.\n"
    
                raise DetailedContextSizeError(error_message) from e
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/smat-dev/jinni'

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