main.py•24.5 kB
"""
IDE Chat Summarizer MCP Server
This MCP server is designed for IDE users (VS Code, Cursor, Visual Studio) to
summarize chat conversations with AI and store the summaries as organized
markdown files in your notes directory.
Usage:
uv run python main.py
Or as MCP server:
uv run mcp run main.py
"""
import os
import json
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Any
from mcp.server.fastmcp import FastMCP
# Create MCP server
mcp = FastMCP("IDE Chat Summarizer")
# Notes directory configuration
# Uses environment variable CHAT_NOTES_DIR if set, otherwise defaults to ~/Documents/ChatSummaries
import os
NOTES_DIR = Path(os.getenv("CHAT_NOTES_DIR", Path.home() / "Documents" / "ChatSummaries"))
# Alternative configurations (uncomment and modify as needed):
# NOTES_DIR = Path("D:/RL/Notes/MarkdownNotes") # Custom absolute path
# NOTES_DIR = Path.cwd() / "summaries" # Relative to current directory
# NOTES_DIR = Path.home() / "Notes" / "ChatSummaries" # User's Notes folder
def ensure_notes_directory():
"""Ensure the notes directory exists"""
NOTES_DIR.mkdir(parents=True, exist_ok=True)
return NOTES_DIR.exists()
def generate_filename(title: str = None) -> str:
"""Generate a filename for the summary"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
if title:
# Clean title for filename
clean_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).rstrip()
clean_title = clean_title.replace(' ', '_')[:50] # Limit length
return f"chat_summary_{timestamp}_{clean_title}.md"
return f"chat_summary_{timestamp}.md"
def extract_code_blocks(text: str) -> List[Dict[str, str]]:
"""Extract code blocks from chat text"""
import re
# Pattern to match code blocks (both ``` and single backticks)
code_block_pattern = r'```(\w+)?\n(.*?)\n```|`([^`]+)`'
code_blocks = []
# Find all code blocks
matches = re.findall(code_block_pattern, text, re.DOTALL)
for match in matches:
if match[0] or match[1]: # Triple backtick code block
language = match[0] if match[0] else "text"
code = match[1].strip()
if code: # Only include non-empty code blocks
code_blocks.append({
"language": language,
"code": code,
"type": "block"
})
elif match[2]: # Inline code
code = match[2].strip()
if code:
code_blocks.append({
"language": "text",
"code": code,
"type": "inline"
})
return code_blocks
def identify_final_solutions(chat_history: str, code_blocks: List[Dict[str, str]]) -> List[Dict[str, str]]:
"""Identify code blocks that appear to be final solutions"""
if not code_blocks:
return []
final_solutions = []
# Keywords that suggest a final solution
solution_keywords = [
"final", "solution", "working", "complete", "fixed", "resolved",
"here's the", "this works", "final version", "complete code",
"final implementation", "working example", "solved"
]
# Look for code blocks that appear near solution keywords
lines = chat_history.split('\n')
for i, code_block in enumerate(code_blocks):
# Check if this code block appears near solution keywords
code_context = ""
# Find the code block in the original text to get context
code_start_idx = chat_history.find(code_block["code"])
if code_start_idx != -1:
# Get 200 characters before and after the code block for context
context_start = max(0, code_start_idx - 200)
context_end = min(len(chat_history), code_start_idx + len(code_block["code"]) + 200)
code_context = chat_history[context_start:context_end].lower()
# Check if any solution keywords appear in the context
is_solution = any(keyword in code_context for keyword in solution_keywords)
# Also consider code blocks that appear later in the conversation as more likely to be solutions
position_weight = (code_start_idx / len(chat_history)) * 100 # Percentage through conversation
if is_solution or position_weight > 70: # If it's marked as solution or appears in last 30% of conversation
final_solutions.append({
**code_block,
"context": code_context[:100] + "..." if len(code_context) > 100 else code_context,
"position_weight": position_weight
})
return final_solutions
@mcp.tool()
def summarize_chat(
chat_history: str,
title: str = None,
summary_style: str = "detailed",
include_full_history: bool = True,
create_separate_full_history: bool = False
) -> str:
"""
Summarize chat history and save it as a markdown file.
Args:
chat_history: The chat conversation text to summarize
title: Optional title for the summary (will be used in filename)
summary_style: Style of summary - 'brief', 'detailed', or 'bullet_points'
include_full_history: Whether to include the full chat history in the summary file (default: True)
create_separate_full_history: Whether to create a separate file with just the full history (default: False)
Returns:
Path to the created summary file and preview of the summary
"""
try:
# Ensure notes directory exists
if not ensure_notes_directory():
return "Error: Could not create or access notes directory"
# Detect code sections and final solutions
code_blocks = extract_code_blocks(chat_history)
final_solutions = identify_final_solutions(chat_history, code_blocks)
has_code = len(code_blocks) > 0
has_solutions = len(final_solutions) > 0
# Generate summary based on style with code awareness
if summary_style == "brief":
if has_code:
summary_prompt = "Provide a brief 2-3 sentence summary of the key points from this conversation. Include any final code solutions or important code snippets that solve the main problem."
else:
summary_prompt = "Provide a brief 2-3 sentence summary of the key points from this conversation."
elif summary_style == "bullet_points":
if has_code:
summary_prompt = "Create a bullet-point summary of the main topics and decisions from this conversation. Include a section for final code solutions and important code snippets."
else:
summary_prompt = "Create a bullet-point summary of the main topics and decisions from this conversation."
else: # detailed
if has_code:
summary_prompt = "Provide a detailed summary including main topics discussed, key decisions made, and important insights from this conversation. Pay special attention to final code solutions, working code examples, and important code snippets that solve the main problems discussed."
else:
summary_prompt = "Provide a detailed summary including main topics discussed, key decisions made, and important insights from this conversation."
# Create summary content (this would typically use an AI model)
# For now, we'll create a structured summary
lines = chat_history.split('\n')
message_count = len([line for line in lines if line.strip()])
# Generate filename
filename = generate_filename(title)
filepath = NOTES_DIR / filename
# Determine content organization based on size
history_size_mb = len(chat_history) / (1024 * 1024)
is_large_history = history_size_mb > 1 # Consider >1MB as large
# Create code solutions section if found
code_solutions_section = ""
if has_solutions:
code_solutions_section = "\n### 💻 Final Code Solutions\n\n"
for i, solution in enumerate(final_solutions, 1):
lang = solution.get("language", "text")
code = solution["code"]
solution_type = "Code Block" if solution["type"] == "block" else "Inline Code"
code_solutions_section += f"**Solution {i} ({solution_type} - {lang}):**\n"
code_solutions_section += f"```{lang}\n{code}\n```\n\n"
elif has_code and not has_solutions:
# Include all code blocks if no specific solutions were identified
code_solutions_section = "\n### 📝 Code Snippets\n\n"
for i, code_block in enumerate(code_blocks[:5], 1): # Limit to first 5 blocks
lang = code_block.get("language", "text")
code = code_block["code"]
code_type = "Code Block" if code_block["type"] == "block" else "Inline Code"
code_solutions_section += f"**Code {i} ({code_type} - {lang}):**\n"
code_solutions_section += f"```{lang}\n{code}\n```\n\n"
# Create summary section
summary_section = f"""## Summary
{summary_prompt}
### Key Points
- Total messages in conversation: {message_count}
- Conversation length: {len(chat_history):,} characters ({history_size_mb:.2f} MB)
- Large history: {'Yes' if is_large_history else 'No'}
- Code blocks found: {len(code_blocks)}
- Final solutions identified: {len(final_solutions)}
{code_solutions_section}"""
# Create markdown content based on options
if include_full_history:
if is_large_history:
# For large histories, put summary first, then full history in collapsible section
markdown_content = f"""# Chat Summary: {title or 'Untitled'}
**Date:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
**Messages:** {message_count}
**Summary Style:** {summary_style.replace('_', ' ').title()}
{summary_section}
## Full Chat History
<details>
<summary>Click to expand full conversation ({history_size_mb:.2f} MB)</summary>
```
{chat_history}
```
</details>
---
*Generated by Chat History Summarizer MCP Server*
"""
else:
# For smaller histories, include everything normally
markdown_content = f"""# Chat Summary: {title or 'Untitled'}
**Date:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
**Messages:** {message_count}
**Summary Style:** {summary_style.replace('_', ' ').title()}
{summary_section}
## Original Chat History
```
{chat_history}
```
---
*Generated by Chat History Summarizer MCP Server*
"""
else:
# Summary only
markdown_content = f"""# Chat Summary: {title or 'Untitled'}
**Date:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
**Messages:** {message_count}
**Summary Style:** {summary_style.replace('_', ' ').title()}
{summary_section}
> **Note:** Full chat history not included in this summary file.
> Original conversation length: {len(chat_history):,} characters ({history_size_mb:.2f} MB)
---
*Generated by Chat History Summarizer MCP Server*
"""
# Save main summary file
with open(filepath, 'w', encoding='utf-8') as f:
f.write(markdown_content)
result_message = f"Summary saved to: {filepath}"
# Create separate full history file if requested
if create_separate_full_history:
history_filename = filepath.stem.replace("chat_summary_", "chat_full_") + ".md"
history_filepath = NOTES_DIR / history_filename
full_history_content = f"""# Full Chat History: {title or 'Untitled'}
**Date:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
**Messages:** {message_count}
**Size:** {len(chat_history):,} characters ({history_size_mb:.2f} MB)
## Complete Conversation
```
{chat_history}
```
---
*Full chat history preserved by Chat History Summarizer MCP Server*
"""
with open(history_filepath, 'w', encoding='utf-8') as f:
f.write(full_history_content)
result_message += f"\nFull history saved separately to: {history_filepath}"
preview = markdown_content[:500] + "..." if len(markdown_content) > 500 else markdown_content
return f"{result_message}\n\nPreview:\n{preview}"
except Exception as e:
return f"Error creating summary: {str(e)}"
@mcp.tool()
def summarize_large_chat(
chat_history: str,
title: str = None,
chunk_size: int = 50000,
overlap: int = 5000
) -> str:
"""
Handle extremely large chat histories by chunking them into manageable pieces.
Each chunk gets its own summary, then creates a master summary.
Args:
chat_history: The large chat conversation text to summarize
title: Optional title for the summary
chunk_size: Size of each chunk in characters (default: 50,000)
overlap: Overlap between chunks in characters (default: 5,000)
Returns:
Information about the chunked summaries created
"""
try:
if not ensure_notes_directory():
return "Error: Could not create or access notes directory"
history_size = len(chat_history)
if history_size <= chunk_size:
return f"Chat history ({history_size:,} chars) is small enough for regular summarization. Use summarize_chat instead."
# Calculate chunks
chunks = []
start = 0
chunk_num = 1
while start < len(chat_history):
end = min(start + chunk_size, len(chat_history))
chunk_text = chat_history[start:end]
chunks.append({
'number': chunk_num,
'text': chunk_text,
'start': start,
'end': end,
'size': len(chunk_text)
})
start = end - overlap # Overlap to maintain context
chunk_num += 1
# Create individual chunk summaries
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
clean_title = title.replace(' ', '_')[:30] if title else "Large_Chat"
chunk_summaries = []
chunk_files = []
for chunk in chunks:
chunk_filename = f"chat_chunk_{timestamp}_{clean_title}_part{chunk['number']:02d}.md"
chunk_filepath = NOTES_DIR / chunk_filename
chunk_content = f"""# Chat Chunk {chunk['number']}: {title or 'Large Chat'}
**Date:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
**Chunk:** {chunk['number']} of {len(chunks)}
**Characters:** {chunk['start']:,} - {chunk['end']:,} ({chunk['size']:,} chars)
**Total Size:** {history_size:,} characters
## Chunk Summary
This is part {chunk['number']} of a large conversation that was split into {len(chunks)} chunks for processing.
### Key Points from This Chunk
- Chunk size: {chunk['size']:,} characters
- Position in conversation: {(chunk['start']/history_size)*100:.1f}% - {(chunk['end']/history_size)*100:.1f}%
## Chunk Content
```
{chunk['text']}
```
---
*Generated by Chat History Summarizer MCP Server - Large Chat Handler*
"""
with open(chunk_filepath, 'w', encoding='utf-8') as f:
f.write(chunk_content)
chunk_files.append(chunk_filename)
chunk_summaries.append(f"Chunk {chunk['number']}: {chunk['size']:,} chars ({chunk['start']:,}-{chunk['end']:,})")
# Create master summary file
master_filename = f"chat_master_{timestamp}_{clean_title}.md"
master_filepath = NOTES_DIR / master_filename
master_content = f"""# Master Summary: {title or 'Large Chat'}
**Date:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
**Total Size:** {history_size:,} characters ({history_size/(1024*1024):.2f} MB)
**Chunks Created:** {len(chunks)}
**Chunk Size:** {chunk_size:,} characters
**Overlap:** {overlap:,} characters
## Overview
This large conversation was automatically split into {len(chunks)} chunks for better handling and processing.
## Chunk Breakdown
{chr(10).join(f"- **{summary}**" for summary in chunk_summaries)}
## Chunk Files Created
{chr(10).join(f"- `{filename}`" for filename in chunk_files)}
## Usage Instructions
1. **Read individual chunks** for detailed content
2. **Search across chunks** to find specific topics
3. **Use chunk summaries** for quick reference
4. **Combine insights** from multiple chunks as needed
## Master Summary
> **Note:** For extremely large conversations, consider reading individual chunks for complete context.
> This master file provides an overview of the conversation structure.
---
*Generated by Chat History Summarizer MCP Server - Large Chat Handler*
"""
with open(master_filepath, 'w', encoding='utf-8') as f:
f.write(master_content)
return f"""Large chat processed successfully!
**Master Summary:** {master_filepath}
**Total Size:** {history_size:,} characters ({history_size/(1024*1024):.2f} MB)
**Chunks Created:** {len(chunks)}
**Files Created:**
- Master: {master_filename}
{chr(10).join(f"- Chunk {i+1}: {filename}" for i, filename in enumerate(chunk_files))}
All chunks preserve the complete original conversation with overlapping context for continuity."""
except Exception as e:
return f"Error processing large chat: {str(e)}"
@mcp.tool()
def list_summaries(limit: int = 10) -> str:
"""
List recent chat summaries from the notes directory.
Args:
limit: Maximum number of summaries to list (default: 10)
Returns:
List of recent summary files with their creation dates
"""
try:
if not ensure_notes_directory():
return "Error: Could not access notes directory"
# Find all chat summary files
summary_files = list(NOTES_DIR.glob("chat_summary_*.md"))
if not summary_files:
return "No chat summaries found in the notes directory."
# Sort by modification time (newest first)
summary_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
# Limit results
summary_files = summary_files[:limit]
result = f"Recent Chat Summaries (showing {len(summary_files)} of {len(list(NOTES_DIR.glob('chat_summary_*.md')))} total):\n\n"
for file in summary_files:
# Get file stats
stat = file.stat()
created = datetime.fromtimestamp(stat.st_ctime).strftime("%Y-%m-%d %H:%M")
size_kb = round(stat.st_size / 1024, 1)
# Try to extract title from filename
name_parts = file.stem.replace("chat_summary_", "").split("_", 2)
display_name = name_parts[2] if len(name_parts) > 2 else "Untitled"
display_name = display_name.replace("_", " ")
result += f"**{display_name}**\n"
result += f" {created} | {size_kb} KB | {file.name}\n\n"
return result
except Exception as e:
return f"Error listing summaries: {str(e)}"
@mcp.tool()
def delete_summary(filename: str) -> str:
"""
Delete a chat summary file.
Args:
filename: Name of the summary file to delete
Returns:
Confirmation message
"""
try:
if not ensure_notes_directory():
return "Error: Could not access notes directory"
filepath = NOTES_DIR / filename
if not filepath.exists():
return f"File not found: {filename}"
if not filepath.name.startswith("chat_summary_"):
return f"Can only delete chat summary files. File must start with 'chat_summary_'"
filepath.unlink()
return f"Successfully deleted: {filename}"
except Exception as e:
return f"Error deleting summary: {str(e)}"
@mcp.resource("summary://{filename}")
def get_summary_content(filename: str) -> str:
"""
Get the content of a specific chat summary file.
Args:
filename: Name of the summary file
Returns:
Content of the summary file
"""
try:
if not ensure_notes_directory():
return "Error: Could not access notes directory"
filepath = NOTES_DIR / filename
if not filepath.exists():
return f"File not found: {filename}"
with open(filepath, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
return f"Error reading summary: {str(e)}"
@mcp.resource("notes://directory")
def get_notes_directory_info() -> str:
"""Get information about the notes directory"""
try:
if not ensure_notes_directory():
return "Error: Could not access notes directory"
total_files = len(list(NOTES_DIR.glob("*.md")))
summary_files = len(list(NOTES_DIR.glob("chat_summary_*.md")))
other_files = total_files - summary_files
info = f"""# Notes Directory Information
**Path:** {NOTES_DIR}
**Total Markdown Files:** {total_files}
**Chat Summaries:** {summary_files}
**Other Files:** {other_files}
**Directory Size:** {sum(f.stat().st_size for f in NOTES_DIR.rglob('*') if f.is_file()) / 1024:.1f} KB
## Recent Activity
Last modified: {datetime.fromtimestamp(max((f.stat().st_mtime for f in NOTES_DIR.rglob('*') if f.is_file()), default=0)).strftime("%Y-%m-%d %H:%M:%S") if any(NOTES_DIR.rglob('*')) else 'No files'}
"""
return info
except Exception as e:
return f"Error getting directory info: {str(e)}"
@mcp.prompt()
def create_summary_prompt(
conversation_type: str = "general",
focus_area: str = "all"
) -> str:
"""
Generate a prompt for creating chat summaries.
Args:
conversation_type: Type of conversation (general, technical, meeting, brainstorm)
focus_area: What to focus on (all, decisions, action_items, insights)
Returns:
A customized prompt for summarizing the conversation
"""
base_prompts = {
"general": "Please summarize this general conversation, highlighting the main topics discussed and key points made.",
"technical": "Please summarize this technical discussion, focusing on solutions proposed, technologies mentioned, and implementation details.",
"meeting": "Please summarize this meeting, emphasizing decisions made, action items assigned, and next steps.",
"brainstorm": "Please summarize this brainstorming session, capturing the ideas generated, creative solutions proposed, and potential directions explored."
}
focus_additions = {
"decisions": " Pay special attention to any decisions that were made and their rationale.",
"action_items": " Highlight any action items, tasks, or follow-up work that was identified.",
"insights": " Focus on key insights, learnings, and important realizations that emerged.",
"all": " Include all important aspects: decisions, action items, insights, and key discussion points."
}
base_prompt = base_prompts.get(conversation_type, base_prompts["general"])
focus_addition = focus_additions.get(focus_area, focus_additions["all"])
return base_prompt + focus_addition + " Structure the summary clearly with appropriate headings and bullet points where helpful."
def main():
"""Run the FastMCP server"""
import sys
# Set UTF-8 encoding for stdout to handle emojis on Windows
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8')
try:
print("🚀 Starting IDE Chat Summarizer MCP Server...")
print(f"📁 Notes directory: {NOTES_DIR}")
except UnicodeEncodeError:
# Fallback without emojis for systems that can't handle them
print("Starting IDE Chat Summarizer MCP Server...")
print(f"Notes directory: {NOTES_DIR}")
# Run the FastMCP server
mcp.run()
if __name__ == "__main__":
main()