From 3dffce6904b60b0b5dbff21709e794de4c8cda76 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Thu, 21 Aug 2025 02:50:04 -0600 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Add=20aggressive=20content=20limiti?= =?UTF-8?q?ng=20to=20prevent=20MCP=2025k=20token=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement smart content truncation at ~80k chars (~20k tokens) - Preserve document structure when truncating (stop at natural breaks) - Add clear truncation notices with guidance for smaller ranges - Update chunking suggestions to use safer 8-page chunks - Enhance recommendations to suggest 3-8 page ranges - Prevent 29,869 > 25,000 token errors while maintaining usability --- src/mcp_office_tools/server.py | 83 ++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/src/mcp_office_tools/server.py b/src/mcp_office_tools/server.py index 80ba6b1..fea7319 100644 --- a/src/mcp_office_tools/server.py +++ b/src/mcp_office_tools/server.py @@ -375,10 +375,27 @@ async def convert_to_markdown( if "table_of_contents" in markdown_result: result["table_of_contents"] = markdown_result["table_of_contents"] else: - # Include full content for smaller documents or page ranges - result["markdown"] = markdown_result["content"] - result["metadata"]["character_count"] = len(markdown_result["content"]) - result["metadata"]["word_count"] = len(markdown_result["content"].split()) + # Include content with automatic size limiting to prevent MCP errors + content = markdown_result["content"] + + # Apply aggressive content limiting to stay under 25k token limit + # Rough estimate: ~4 chars per token, leave buffer for metadata + max_content_chars = 80000 # ~20k tokens worth of content + + if len(content) > max_content_chars: + # Truncate but try to preserve structure + truncated_content = _smart_truncate_content(content, max_content_chars) + result["markdown"] = truncated_content + result["content_truncated"] = True + result["original_length"] = len(content) + result["truncated_length"] = len(truncated_content) + result["truncation_note"] = f"Content truncated to stay under MCP 25k token limit. Original: {len(content):,} chars, Shown: {len(truncated_content):,} chars. Use smaller page ranges for full content." + else: + result["markdown"] = content + result["content_truncated"] = False + + result["metadata"]["character_count"] = len(content) + result["metadata"]["word_count"] = len(content.split()) # Add image info if include_images and markdown_result.get("images"): @@ -1522,6 +1539,49 @@ def _extract_markdown_structure(content: str) -> dict[str, Any]: return structure +def _smart_truncate_content(content: str, max_chars: int) -> str: + """Intelligently truncate content while preserving structure and readability.""" + if len(content) <= max_chars: + return content + + lines = content.split('\n') + truncated_lines = [] + current_length = 0 + + # Try to preserve structure by stopping at a natural break point + for line in lines: + line_length = len(line) + 1 # +1 for newline + + # If adding this line would exceed limit + if current_length + line_length > max_chars: + # Try to find a good stopping point + if truncated_lines: + # Check if we're in the middle of a section + last_lines = '\n'.join(truncated_lines[-3:]) if len(truncated_lines) >= 3 else '\n'.join(truncated_lines) + + # If we stopped mid-paragraph, remove incomplete paragraph + if not (line.strip() == '' or line.startswith('#') or line.startswith('|')): + # Remove lines until we hit a natural break + while truncated_lines and not ( + truncated_lines[-1].strip() == '' or + truncated_lines[-1].startswith('#') or + truncated_lines[-1].startswith('|') or + truncated_lines[-1].startswith('-') or + truncated_lines[-1].startswith('*') + ): + truncated_lines.pop() + break + + truncated_lines.append(line) + current_length += line_length + + # Add truncation notice + result = '\n'.join(truncated_lines) + result += f"\n\n---\n**[CONTENT TRUNCATED]**\nShowing {len(result):,} of {len(content):,} characters.\nUse smaller page ranges (e.g., 3-5 pages) for full content without truncation.\n---" + + return result + + def _estimate_section_length(heading_level: int) -> int: """Estimate how many pages a section might span based on heading level.""" # Higher level headings (H1) tend to have longer sections @@ -1598,7 +1658,8 @@ def _generate_chunking_suggestions(sections: list) -> list[dict[str, Any]]: section_pages = section["estimated_end_page"] - section["start_page"] + 1 # If adding this section would make chunk too large, finalize current chunk - if current_chunk_pages + section_pages > 15 and chunk_sections: + # Use smaller chunks (8 pages) to prevent MCP token limit issues + if current_chunk_pages + section_pages > 8 and chunk_sections: suggestions.append({ "chunk_number": len(suggestions) + 1, "page_range": f"{chunk_start}-{chunk_sections[-1]['estimated_end_page']}", @@ -1761,13 +1822,15 @@ def _get_processing_recommendation( "Consider using recommended workflow for better performance." ) recommendation["suggested_workflow"] = [ - "1. First: Call with summary_only=true to get document overview", - "2. Then: Use page_range to process specific sections (e.g., '1-10', '20-30')", - "3. Alternative: Process in chunks of 10-15 pages to avoid response limits" + "1. First: Call with summary_only=true to get document overview and TOC", + "2. Then: Use page_range to process specific sections (e.g., '1-5', '6-10', '15-20')", + "3. Recommended: Use 3-8 page chunks to stay under 25k token MCP limit", + "4. The tool auto-truncates if content is too large, but smaller ranges work better" ] recommendation["warnings"] = [ - "Full document processing may hit 25k token response limit", - "Large responses may be slow and consume significant resources" + "Page ranges >8 pages may hit 25k token response limit and get truncated", + "Use smaller page ranges (3-5 pages) for dense content documents", + "Auto-truncation preserves structure but loses content completeness" ] # Medium document recommendations