mcp-code-indexer 1.4.1__tar.gz → 1.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {mcp_code_indexer-1.4.1/src/mcp_code_indexer.egg-info → mcp_code_indexer-1.6.0}/PKG-INFO +25 -3
  2. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/README.md +24 -2
  3. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/docs/git-hook-setup.md +79 -2
  4. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/pyproject.toml +1 -1
  5. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/setup.py +1 -1
  6. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/__init__.py +1 -1
  7. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/database/database.py +120 -0
  8. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/git_hook_handler.py +165 -18
  9. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/main.py +36 -8
  10. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0/src/mcp_code_indexer.egg-info}/PKG-INFO +25 -3
  11. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/LICENSE +0 -0
  12. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/MANIFEST.in +0 -0
  13. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/docs/api-reference.md +0 -0
  14. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/docs/architecture.md +0 -0
  15. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/docs/configuration.md +0 -0
  16. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/docs/contributing.md +0 -0
  17. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/migrations/001_initial.sql +0 -0
  18. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/migrations/002_performance_indexes.sql +0 -0
  19. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/migrations/003_project_overviews.sql +0 -0
  20. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/requirements.txt +0 -0
  21. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/setup.cfg +0 -0
  22. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/__main__.py +0 -0
  23. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/data/stop_words_english.txt +0 -0
  24. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/database/__init__.py +0 -0
  25. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/database/models.py +0 -0
  26. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/error_handler.py +0 -0
  27. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/file_scanner.py +0 -0
  28. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/logging_config.py +0 -0
  29. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/merge_handler.py +0 -0
  30. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/middleware/__init__.py +0 -0
  31. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/middleware/error_middleware.py +0 -0
  32. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/server/__init__.py +0 -0
  33. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/server/mcp_server.py +0 -0
  34. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +0 -0
  35. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/token_counter.py +0 -0
  36. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer/tools/__init__.py +0 -0
  37. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer.egg-info/SOURCES.txt +0 -0
  38. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer.egg-info/dependency_links.txt +0 -0
  39. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer.egg-info/entry_points.txt +0 -0
  40. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer.egg-info/requires.txt +0 -0
  41. {mcp_code_indexer-1.4.1 → mcp_code_indexer-1.6.0}/src/mcp_code_indexer.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-code-indexer
3
- Version: 1.4.1
3
+ Version: 1.6.0
4
4
  Summary: MCP server that tracks file descriptions across codebases, enabling AI agents to efficiently navigate and understand code through searchable summaries and token-aware overviews.
5
5
  Author: MCP Code Indexer Contributors
6
6
  Maintainer: MCP Code Indexer Contributors
@@ -57,8 +57,8 @@ Dynamic: requires-python
57
57
 
58
58
  # MCP Code Indexer 🚀
59
59
 
60
- [![PyPI version](https://badge.fury.io/py/mcp-code-indexer.svg?2)](https://badge.fury.io/py/mcp-code-indexer)
61
- [![Python](https://img.shields.io/pypi/pyversions/mcp-code-indexer.svg?2)](https://pypi.org/project/mcp-code-indexer/)
60
+ [![PyPI version](https://badge.fury.io/py/mcp-code-indexer.svg?4)](https://badge.fury.io/py/mcp-code-indexer)
61
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-code-indexer.svg?4)](https://pypi.org/project/mcp-code-indexer/)
62
62
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
63
63
 
64
64
  A production-ready **Model Context Protocol (MCP) server** that revolutionizes how AI agents navigate and understand codebases. Instead of repeatedly scanning files, agents get instant access to intelligent descriptions, semantic search, and context-aware recommendations.
@@ -246,6 +246,28 @@ The server provides **11 powerful MCP tools** for intelligent codebase managemen
246
246
 
247
247
  💡 **Pro Tip**: Always start with `check_codebase_size` to get personalized recommendations for navigating your specific codebase.
248
248
 
249
+ ## 🔗 Git Hook Integration
250
+
251
+ Keep your codebase documentation automatically synchronized with automated analysis on every commit, rebase, or merge:
252
+
253
+ ```bash
254
+ # Analyze current staged changes
255
+ mcp-code-indexer --githook
256
+
257
+ # Analyze a specific commit
258
+ mcp-code-indexer --githook abc123def
259
+
260
+ # Analyze a commit range (perfect for rebases)
261
+ mcp-code-indexer --githook abc123 def456
262
+ ```
263
+
264
+ **🎯 Perfect for**:
265
+ - **Automated documentation** that never goes stale
266
+ - **Rebase-aware analysis** that handles complex git operations
267
+ - **Zero-effort maintenance** with background processing
268
+
269
+ See the **[Git Hook Setup Guide](docs/git-hook-setup.md)** for complete installation instructions including post-commit, post-merge, and post-rewrite hooks.
270
+
249
271
  ## 🏗️ Architecture Highlights
250
272
 
251
273
  ### Performance Optimized
@@ -1,7 +1,7 @@
1
1
  # MCP Code Indexer 🚀
2
2
 
3
- [![PyPI version](https://badge.fury.io/py/mcp-code-indexer.svg?2)](https://badge.fury.io/py/mcp-code-indexer)
4
- [![Python](https://img.shields.io/pypi/pyversions/mcp-code-indexer.svg?2)](https://pypi.org/project/mcp-code-indexer/)
3
+ [![PyPI version](https://badge.fury.io/py/mcp-code-indexer.svg?4)](https://badge.fury.io/py/mcp-code-indexer)
4
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-code-indexer.svg?4)](https://pypi.org/project/mcp-code-indexer/)
5
5
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
7
  A production-ready **Model Context Protocol (MCP) server** that revolutionizes how AI agents navigate and understand codebases. Instead of repeatedly scanning files, agents get instant access to intelligent descriptions, semantic search, and context-aware recommendations.
@@ -189,6 +189,28 @@ The server provides **11 powerful MCP tools** for intelligent codebase managemen
189
189
 
190
190
  💡 **Pro Tip**: Always start with `check_codebase_size` to get personalized recommendations for navigating your specific codebase.
191
191
 
192
+ ## 🔗 Git Hook Integration
193
+
194
+ Keep your codebase documentation automatically synchronized with automated analysis on every commit, rebase, or merge:
195
+
196
+ ```bash
197
+ # Analyze current staged changes
198
+ mcp-code-indexer --githook
199
+
200
+ # Analyze a specific commit
201
+ mcp-code-indexer --githook abc123def
202
+
203
+ # Analyze a commit range (perfect for rebases)
204
+ mcp-code-indexer --githook abc123 def456
205
+ ```
206
+
207
+ **🎯 Perfect for**:
208
+ - **Automated documentation** that never goes stale
209
+ - **Rebase-aware analysis** that handles complex git operations
210
+ - **Zero-effort maintenance** with background processing
211
+
212
+ See the **[Git Hook Setup Guide](docs/git-hook-setup.md)** for complete installation instructions including post-commit, post-merge, and post-rewrite hooks.
213
+
192
214
  ## 🏗️ Architecture Highlights
193
215
 
194
216
  ### Performance Optimized
@@ -40,11 +40,25 @@ Before setting up automatic hooks, test the functionality manually:
40
40
  # Navigate to your git project
41
41
  cd /path/to/your/project
42
42
 
43
- # Test git hook analysis on recent changes
43
+ # Test git hook analysis on recent changes (staged changes)
44
44
  mcp-code-indexer --githook
45
+
46
+ # Or analyze a specific commit
47
+ mcp-code-indexer --githook abc123def
48
+
49
+ # Or analyze a range of commits (useful for rebases)
50
+ mcp-code-indexer --githook abc123..def456
45
51
  ```
46
52
 
47
- **What you'll see**: The tool analyzes recent git changes and updates descriptions for modified files.
53
+ **What you'll see**: The tool analyzes git changes and updates descriptions for modified files.
54
+
55
+ ### Usage Modes
56
+
57
+ The `--githook` command supports three modes:
58
+
59
+ 1. **Current Changes** (default): `--githook` - Analyzes staged changes
60
+ 2. **Specific Commit**: `--githook COMMIT_HASH` - Analyzes a particular commit
61
+ 3. **Commit Range**: `--githook HASH1 HASH2` - Analyzes all commits from HASH1 to HASH2
48
62
 
49
63
  ### Automatic Setup
50
64
 
@@ -90,6 +104,69 @@ Make it executable:
90
104
  chmod +x .git/hooks/post-merge
91
105
  ```
92
106
 
107
+ #### Post-Rewrite Hook (Recommended for Rebases)
108
+
109
+ For handling **rebases**, **amends**, and other commit-rewriting operations, use the `post-rewrite` hook. This ensures descriptions are updated for all rewritten commits:
110
+
111
+ Create `.git/hooks/post-rewrite`:
112
+
113
+ ```bash
114
+ #!/bin/bash
115
+ set -eo pipefail
116
+
117
+ # MCP Code Indexer - Auto-update descriptions after rebase/amend
118
+
119
+ cd "$(git rev-parse --show-toplevel)"
120
+
121
+ # Read the list of rewritten commits from stdin
122
+ first_hash=""
123
+ last_hash=""
124
+ count=0
125
+
126
+ while read -r old_sha new_sha; do
127
+ # Skip invalid lines (e.g., comments)
128
+ [[ "$old_sha" =~ ^# ]] && continue
129
+
130
+ # Track first and last commits
131
+ if [ $count -eq 0 ]; then
132
+ first_hash="$new_sha"
133
+ fi
134
+ last_hash="$new_sha"
135
+ ((count++))
136
+ done < "$1"
137
+
138
+ # Process the commit range if we have commits
139
+ if [ $count -gt 0 ]; then
140
+ if [ $count -eq 1 ]; then
141
+ # Single commit
142
+ mcp-code-indexer --githook "$first_hash" >> ~/.mcp-code-index/githook.log 2>&1 &
143
+ else
144
+ # Commit range - use first commit's parent to last commit
145
+ parent_hash=$(git rev-parse "${first_hash}~1" 2>/dev/null || echo "")
146
+ if [ -n "$parent_hash" ]; then
147
+ mcp-code-indexer --githook "$parent_hash" "$last_hash" >> ~/.mcp-code-index/githook.log 2>&1 &
148
+ else
149
+ # If no parent (first commit), process each individually
150
+ mcp-code-indexer --githook "$first_hash" >> ~/.mcp-code-index/githook.log 2>&1 &
151
+ fi
152
+ fi
153
+ disown # Detach background process
154
+ fi
155
+
156
+ exit 0
157
+ ```
158
+
159
+ Make it executable:
160
+ ```bash
161
+ chmod +x .git/hooks/post-rewrite
162
+ ```
163
+
164
+ **🎯 Why Post-Rewrite?**
165
+ - **Handles rebases**: Processes all rewritten commits after `git rebase`
166
+ - **Non-blocking**: Runs in background, doesn't slow down git operations
167
+ - **Efficient**: Processes commit ranges instead of individual commits
168
+ - **Comprehensive**: Works with `git commit --amend`, interactive rebases, etc.
169
+
93
170
  ## 🔧 Configuration Options
94
171
 
95
172
  ### Environment Variables
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mcp-code-indexer"
7
- version = "1.4.1"
7
+ version = "1.6.0"
8
8
  description = "MCP server that tracks file descriptions across codebases, enabling AI agents to efficiently navigate and understand code through searchable summaries and token-aware overviews."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -21,7 +21,7 @@ def get_version():
21
21
  return data["project"]["version"]
22
22
  except Exception:
23
23
  # Fallback version if reading fails
24
- return "1.4.1"
24
+ return "1.6.0"
25
25
 
26
26
  setup(
27
27
  name="mcp-code-indexer",
@@ -6,7 +6,7 @@ intelligent codebase navigation through searchable file descriptions,
6
6
  token-aware overviews, and advanced merge capabilities.
7
7
  """
8
8
 
9
- __version__ = "1.4.1"
9
+ __version__ = "1.6.0"
10
10
  __author__ = "MCP Code Indexer Contributors"
11
11
  __email__ = ""
12
12
  __license__ = "MIT"
@@ -179,6 +179,126 @@ class DatabaseManager:
179
179
  last_accessed=datetime.fromisoformat(row['last_accessed'])
180
180
  )
181
181
  return None
182
+
183
+ async def find_matching_project(
184
+ self,
185
+ project_name: str,
186
+ remote_origin: Optional[str] = None,
187
+ upstream_origin: Optional[str] = None,
188
+ folder_path: Optional[str] = None
189
+ ) -> Optional[Project]:
190
+ """
191
+ Find project by matching criteria.
192
+
193
+ Args:
194
+ project_name: Name of the project
195
+ remote_origin: Remote origin URL
196
+ upstream_origin: Upstream origin URL
197
+ folder_path: Project folder path
198
+
199
+ Returns:
200
+ Matching project or None
201
+ """
202
+ projects = await self.get_all_projects()
203
+ normalized_name = project_name.lower()
204
+
205
+ best_match = None
206
+ best_score = 0
207
+
208
+ for project in projects:
209
+ score = 0
210
+ match_factors = []
211
+
212
+ # Check name match (case-insensitive)
213
+ if project.name.lower() == normalized_name:
214
+ score += 1
215
+ match_factors.append("name")
216
+
217
+ # Check remote origin match
218
+ if remote_origin and project.remote_origin == remote_origin:
219
+ score += 1
220
+ match_factors.append("remote_origin")
221
+
222
+ # Check upstream origin match
223
+ if upstream_origin and project.upstream_origin == upstream_origin:
224
+ score += 1
225
+ match_factors.append("upstream_origin")
226
+
227
+ # Check folder path in aliases
228
+ if folder_path and folder_path in project.aliases:
229
+ score += 1
230
+ match_factors.append("folder_path")
231
+
232
+ # Enhanced matching: If name matches and no remote origins are provided,
233
+ # consider it a strong match to prevent duplicates
234
+ if (score == 1 and "name" in match_factors and
235
+ not remote_origin and not project.remote_origin and
236
+ not upstream_origin and not project.upstream_origin):
237
+ logger.info(f"Name-only match with no remotes for project {project.name} - treating as strong match")
238
+ score = 2 # Boost score to strong match level
239
+ match_factors.append("no_remotes_boost")
240
+
241
+ # If we have 2+ matches, this is a strong candidate
242
+ if score >= 2:
243
+ if score > best_score:
244
+ best_score = score
245
+ best_match = project
246
+ logger.info(f"Strong match for project {project.name} (score: {score}, factors: {match_factors})")
247
+
248
+ return best_match
249
+
250
+ async def get_or_create_project(
251
+ self,
252
+ project_name: str,
253
+ folder_path: str,
254
+ remote_origin: Optional[str] = None,
255
+ upstream_origin: Optional[str] = None
256
+ ) -> Project:
257
+ """
258
+ Get or create a project using intelligent matching.
259
+
260
+ Args:
261
+ project_name: Name of the project
262
+ folder_path: Project folder path
263
+ remote_origin: Remote origin URL
264
+ upstream_origin: Upstream origin URL
265
+
266
+ Returns:
267
+ Existing or newly created project
268
+ """
269
+ # Try to find existing project
270
+ project = await self.find_matching_project(
271
+ project_name, remote_origin, upstream_origin, folder_path
272
+ )
273
+
274
+ if project:
275
+ # Update aliases if folder path not already included
276
+ if folder_path not in project.aliases:
277
+ project.aliases.append(folder_path)
278
+ await self.update_project(project)
279
+ logger.info(f"Added folder path {folder_path} to project {project.name} aliases")
280
+
281
+ # Update access time
282
+ await self.update_project_access_time(project.id)
283
+ return project
284
+
285
+ # Create new project
286
+ from ..database.models import Project
287
+ import uuid
288
+
289
+ new_project = Project(
290
+ id=str(uuid.uuid4()),
291
+ name=project_name,
292
+ remote_origin=remote_origin,
293
+ upstream_origin=upstream_origin,
294
+ aliases=[folder_path],
295
+ created=datetime.utcnow(),
296
+ last_accessed=datetime.utcnow()
297
+ )
298
+
299
+ await self.create_project(new_project)
300
+ logger.info(f"Created new project: {new_project.name} ({new_project.id})")
301
+ return new_project
182
302
 
183
303
  async def update_project_access_time(self, project_id: str) -> None:
184
304
  """Update the last accessed time for a project."""
@@ -73,18 +73,34 @@ class GitHookHandler:
73
73
  if not self.api_key:
74
74
  raise GitHookError("OPENROUTER_API_KEY environment variable is required for git hook mode")
75
75
 
76
- async def run_githook_mode(self) -> None:
76
+ async def run_githook_mode(
77
+ self,
78
+ commit_hash: Optional[str] = None,
79
+ commit_range: Optional[Tuple[str, str]] = None
80
+ ) -> None:
77
81
  """
78
82
  Run in git hook mode - analyze changes and update descriptions.
79
83
 
84
+ Args:
85
+ commit_hash: Process a specific commit by hash
86
+ commit_range: Process commits in range (start_hash, end_hash)
87
+
80
88
  This is the main entry point for git hook functionality.
81
89
  """
82
90
  try:
83
91
  # Get git info from current directory
84
92
  project_info = await self._identify_project_from_git()
85
93
 
86
- # Get git diff
87
- git_diff = await self._get_git_diff()
94
+ # Get git diff and commit message based on mode
95
+ if commit_hash:
96
+ git_diff = await self._get_git_diff_for_commit(commit_hash)
97
+ commit_message = await self._get_commit_message_for_commit(commit_hash)
98
+ elif commit_range:
99
+ git_diff = await self._get_git_diff_for_range(commit_range[0], commit_range[1])
100
+ commit_message = await self._get_commit_messages_for_range(commit_range[0], commit_range[1])
101
+ else:
102
+ git_diff = await self._get_git_diff()
103
+ commit_message = await self._get_commit_message()
88
104
 
89
105
  if not git_diff or len(git_diff) > self.config["max_diff_size"]:
90
106
  self.logger.info(f"Skipping git hook update - diff too large or empty")
@@ -102,6 +118,7 @@ class GitHookHandler:
102
118
  # Build prompt for OpenRouter
103
119
  prompt = self._build_githook_prompt(
104
120
  git_diff,
121
+ commit_message,
105
122
  current_overview,
106
123
  current_descriptions,
107
124
  changed_files
@@ -204,6 +221,115 @@ class GitHookHandler:
204
221
  return diff_result
205
222
  except subprocess.CalledProcessError as e:
206
223
  raise GitHookError(f"Failed to get git diff: {e}")
224
+
225
+ async def _get_commit_message(self) -> str:
226
+ """
227
+ Get the commit message for context about what was changed.
228
+
229
+ Returns:
230
+ Commit message as string
231
+ """
232
+ try:
233
+ # Get the commit message from the latest commit
234
+ message_result = await self._run_git_command([
235
+ "log", "-1", "--pretty=%B"
236
+ ])
237
+ return message_result.strip()
238
+
239
+ except subprocess.CalledProcessError:
240
+ # If no commits exist yet, return empty string
241
+ return ""
242
+
243
+ async def _get_git_diff_for_commit(self, commit_hash: str) -> str:
244
+ """
245
+ Get git diff for a specific commit.
246
+
247
+ Args:
248
+ commit_hash: The commit hash to analyze
249
+
250
+ Returns:
251
+ Git diff content as string
252
+ """
253
+ try:
254
+ # Get diff for the specific commit compared to its parent
255
+ diff_result = await self._run_git_command([
256
+ "diff", "--no-color", "--no-ext-diff", f"{commit_hash}~1..{commit_hash}"
257
+ ])
258
+ return diff_result
259
+
260
+ except subprocess.CalledProcessError:
261
+ # If parent doesn't exist (first commit), diff against empty tree
262
+ try:
263
+ diff_result = await self._run_git_command([
264
+ "diff", "--no-color", "--no-ext-diff", "4b825dc642cb6eb9a060e54bf8d69288fbee4904", commit_hash
265
+ ])
266
+ return diff_result
267
+ except subprocess.CalledProcessError as e:
268
+ raise GitHookError(f"Failed to get git diff for commit {commit_hash}: {e}")
269
+
270
+ async def _get_git_diff_for_range(self, start_hash: str, end_hash: str) -> str:
271
+ """
272
+ Get git diff for a range of commits.
273
+
274
+ Args:
275
+ start_hash: Starting commit hash (exclusive)
276
+ end_hash: Ending commit hash (inclusive)
277
+
278
+ Returns:
279
+ Git diff content as string
280
+ """
281
+ try:
282
+ diff_result = await self._run_git_command([
283
+ "diff", "--no-color", "--no-ext-diff", f"{start_hash}..{end_hash}"
284
+ ])
285
+ return diff_result
286
+ except subprocess.CalledProcessError as e:
287
+ raise GitHookError(f"Failed to get git diff for range {start_hash}..{end_hash}: {e}")
288
+
289
+ async def _get_commit_message_for_commit(self, commit_hash: str) -> str:
290
+ """
291
+ Get the commit message for a specific commit.
292
+
293
+ Args:
294
+ commit_hash: The commit hash
295
+
296
+ Returns:
297
+ Commit message as string
298
+ """
299
+ try:
300
+ message_result = await self._run_git_command([
301
+ "log", "-1", "--pretty=%B", commit_hash
302
+ ])
303
+ return message_result.strip()
304
+ except subprocess.CalledProcessError as e:
305
+ raise GitHookError(f"Failed to get commit message for {commit_hash}: {e}")
306
+
307
+ async def _get_commit_messages_for_range(self, start_hash: str, end_hash: str) -> str:
308
+ """
309
+ Get commit messages for a range of commits.
310
+
311
+ Args:
312
+ start_hash: Starting commit hash (exclusive)
313
+ end_hash: Ending commit hash (inclusive)
314
+
315
+ Returns:
316
+ Combined commit messages as string
317
+ """
318
+ try:
319
+ # Get all commit messages in the range
320
+ message_result = await self._run_git_command([
321
+ "log", "--pretty=%B", f"{start_hash}..{end_hash}"
322
+ ])
323
+
324
+ # Clean up and format the messages
325
+ messages = message_result.strip()
326
+ if messages:
327
+ return f"Combined commit messages for range {start_hash}..{end_hash}:\n\n{messages}"
328
+ else:
329
+ return f"No commits found in range {start_hash}..{end_hash}"
330
+
331
+ except subprocess.CalledProcessError as e:
332
+ raise GitHookError(f"Failed to get commit messages for range {start_hash}..{end_hash}: {e}")
207
333
 
208
334
  def _extract_changed_files(self, git_diff: str) -> List[str]:
209
335
  """
@@ -232,12 +358,12 @@ class GitHookHandler:
232
358
  async def _get_project_overview(self, project_info: Dict[str, Any]) -> str:
233
359
  """Get current project overview from database."""
234
360
  try:
235
- # Try to get existing project
236
- project = await self.db_manager.get_project(
361
+ # Try to find existing project
362
+ project = await self.db_manager.find_matching_project(
237
363
  project_info["projectName"],
238
- project_info["folderPath"],
239
364
  project_info.get("remoteOrigin"),
240
- project_info.get("upstreamOrigin")
365
+ project_info.get("upstreamOrigin"),
366
+ project_info["folderPath"]
241
367
  )
242
368
 
243
369
  if project:
@@ -255,12 +381,12 @@ class GitHookHandler:
255
381
  async def _get_all_descriptions(self, project_info: Dict[str, Any]) -> Dict[str, str]:
256
382
  """Get all current file descriptions from database."""
257
383
  try:
258
- # Try to get existing project
259
- project = await self.db_manager.get_project(
384
+ # Try to find existing project
385
+ project = await self.db_manager.find_matching_project(
260
386
  project_info["projectName"],
261
- project_info["folderPath"],
262
387
  project_info.get("remoteOrigin"),
263
- project_info.get("upstreamOrigin")
388
+ project_info.get("upstreamOrigin"),
389
+ project_info["folderPath"]
264
390
  )
265
391
 
266
392
  if project:
@@ -278,6 +404,7 @@ class GitHookHandler:
278
404
  def _build_githook_prompt(
279
405
  self,
280
406
  git_diff: str,
407
+ commit_message: str,
281
408
  overview: str,
282
409
  descriptions: Dict[str, str],
283
410
  changed_files: List[str]
@@ -287,6 +414,7 @@ class GitHookHandler:
287
414
 
288
415
  Args:
289
416
  git_diff: Git diff content
417
+ commit_message: Commit message explaining the changes
290
418
  overview: Current project overview
291
419
  descriptions: Current file descriptions
292
420
  changed_files: List of changed file paths
@@ -294,7 +422,10 @@ class GitHookHandler:
294
422
  Returns:
295
423
  Formatted prompt for the API
296
424
  """
297
- return f"""Analyze this git diff and update the file descriptions and project overview as needed.
425
+ return f"""Analyze this git commit and update the file descriptions and project overview as needed.
426
+
427
+ COMMIT MESSAGE:
428
+ {commit_message or "No commit message available"}
298
429
 
299
430
  CURRENT PROJECT OVERVIEW:
300
431
  {overview or "No overview available"}
@@ -310,10 +441,12 @@ CHANGED FILES:
310
441
 
311
442
  INSTRUCTIONS:
312
443
 
313
- 1. **File Descriptions**: Update descriptions for any files that have changed significantly. Only include files that need actual description updates.
444
+ Use the COMMIT MESSAGE to understand the intent and context of the changes. The commit message explains what the developer was trying to accomplish.
445
+
446
+ 1. **File Descriptions**: Update descriptions for any files that have changed significantly. Consider both the diff content and the commit message context. Only include files that need actual description updates.
314
447
 
315
448
  2. **Project Overview**: Update ONLY if there are major structural changes like:
316
- - New major features or components
449
+ - New major features or components (which may be indicated by commit message)
317
450
  - Architectural changes (new patterns, frameworks, or approaches)
318
451
  - Significant dependency additions
319
452
  - New API endpoints or workflows
@@ -485,22 +618,36 @@ Return ONLY the JSON, no other text."""
485
618
  # Update file descriptions
486
619
  file_updates = updates.get("file_updates", {})
487
620
  for file_path, description in file_updates.items():
488
- await self.db_manager.upsert_file_description(
621
+ from mcp_code_indexer.database.models import FileDescription
622
+ from datetime import datetime
623
+
624
+ file_desc = FileDescription(
489
625
  project_id=project.id,
490
626
  branch=project_info["branch"],
491
627
  file_path=file_path,
492
- description=description
628
+ description=description,
629
+ file_hash=None,
630
+ last_modified=datetime.utcnow(),
631
+ version=1
493
632
  )
633
+ await self.db_manager.create_file_description(file_desc)
494
634
  self.logger.info(f"Updated description for {file_path}")
495
635
 
496
636
  # Update project overview if provided
497
637
  overview_update = updates.get("overview_update")
498
638
  if overview_update and overview_update.strip():
499
- await self.db_manager.upsert_project_overview(
639
+ from mcp_code_indexer.database.models import ProjectOverview
640
+ from datetime import datetime
641
+
642
+ overview = ProjectOverview(
500
643
  project_id=project.id,
501
644
  branch=project_info["branch"],
502
- overview=overview_update
645
+ overview=overview_update,
646
+ last_modified=datetime.utcnow(),
647
+ total_files=len(file_updates),
648
+ total_tokens=len(overview_update.split())
503
649
  )
650
+ await self.db_manager.create_project_overview(overview)
504
651
  self.logger.info("Updated project overview")
505
652
 
506
653
  except Exception as e:
@@ -82,8 +82,11 @@ def parse_arguments() -> argparse.Namespace:
82
82
 
83
83
  parser.add_argument(
84
84
  "--githook",
85
- action="store_true",
86
- help="Git hook mode: auto-update descriptions based on git diff using OpenRouter API"
85
+ nargs="*",
86
+ metavar="COMMIT_HASH",
87
+ help="Git hook mode: auto-update descriptions based on git diff using OpenRouter API. "
88
+ "Usage: --githook (current changes), --githook HASH (specific commit), "
89
+ "--githook HASH1 HASH2 (commit range from HASH1 to HASH2)"
87
90
  )
88
91
 
89
92
  parser.add_argument(
@@ -462,12 +465,16 @@ async def handle_githook(args: argparse.Namespace) -> None:
462
465
  from .database.database import DatabaseManager
463
466
  from .git_hook_handler import GitHookHandler
464
467
 
468
+ # Process commit hash arguments
469
+ commit_hashes = args.githook if args.githook else []
470
+
465
471
  logger.info("Starting git hook execution", extra={
466
472
  "structured_data": {
467
473
  "args": {
468
474
  "db_path": str(args.db_path),
469
475
  "cache_dir": str(args.cache_dir),
470
- "token_limit": args.token_limit
476
+ "token_limit": args.token_limit,
477
+ "commit_hashes": commit_hashes
471
478
  }
472
479
  }
473
480
  })
@@ -497,7 +504,17 @@ async def handle_githook(args: argparse.Namespace) -> None:
497
504
 
498
505
  # Run git hook analysis
499
506
  logger.info("Starting git hook analysis")
500
- await git_handler.run_githook_mode()
507
+ if len(commit_hashes) == 0:
508
+ # Process current staged changes
509
+ await git_handler.run_githook_mode()
510
+ elif len(commit_hashes) == 1:
511
+ # Process specific commit
512
+ await git_handler.run_githook_mode(commit_hash=commit_hashes[0])
513
+ elif len(commit_hashes) == 2:
514
+ # Process commit range
515
+ await git_handler.run_githook_mode(commit_range=(commit_hashes[0], commit_hashes[1]))
516
+ else:
517
+ raise ValueError("--githook accepts 0, 1, or 2 commit hashes")
501
518
  logger.info("Git hook analysis completed successfully")
502
519
 
503
520
  except Exception as e:
@@ -510,6 +527,14 @@ async def handle_githook(args: argparse.Namespace) -> None:
510
527
  print(f"Git hook error: {e}", file=sys.stderr)
511
528
  sys.exit(1)
512
529
  finally:
530
+ # Clean up database connections
531
+ if 'db_manager' in locals():
532
+ try:
533
+ await db_manager.close_pool()
534
+ logger.debug("Database connections closed")
535
+ except Exception as e:
536
+ logger.warning(f"Error closing database connections: {e}")
537
+
513
538
  logger.info("=== GITHOOK SESSION ENDED ===")
514
539
 
515
540
  # Close logger handlers to flush any remaining logs
@@ -674,15 +699,18 @@ def generate_project_markdown(project, branch, overview, files, logger):
674
699
 
675
700
  markdown_lines = []
676
701
 
677
- # Project header
678
- markdown_lines.append(f"# {project.name}")
702
+ # Project header with sentence case
703
+ project_name = project.name.title() if project.name.islower() else project.name
704
+ markdown_lines.append(f"# {project_name}")
679
705
  markdown_lines.append("")
680
706
 
681
707
  # Project metadata
682
708
  if project.remote_origin:
683
709
  markdown_lines.append(f"**Repository:** {project.remote_origin}")
710
+ markdown_lines.append("")
684
711
  if project.upstream_origin:
685
712
  markdown_lines.append(f"**Upstream:** {project.upstream_origin}")
713
+ markdown_lines.append("")
686
714
  markdown_lines.append(f"**Branch:** {branch}")
687
715
  markdown_lines.append("")
688
716
 
@@ -760,8 +788,8 @@ async def main() -> None:
760
788
  """Main entry point for the MCP server."""
761
789
  args = parse_arguments()
762
790
 
763
- # Handle git hook command
764
- if args.githook:
791
+ # Handle git hook command
792
+ if args.githook is not None:
765
793
  await handle_githook(args)
766
794
  return
767
795
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-code-indexer
3
- Version: 1.4.1
3
+ Version: 1.6.0
4
4
  Summary: MCP server that tracks file descriptions across codebases, enabling AI agents to efficiently navigate and understand code through searchable summaries and token-aware overviews.
5
5
  Author: MCP Code Indexer Contributors
6
6
  Maintainer: MCP Code Indexer Contributors
@@ -57,8 +57,8 @@ Dynamic: requires-python
57
57
 
58
58
  # MCP Code Indexer 🚀
59
59
 
60
- [![PyPI version](https://badge.fury.io/py/mcp-code-indexer.svg?2)](https://badge.fury.io/py/mcp-code-indexer)
61
- [![Python](https://img.shields.io/pypi/pyversions/mcp-code-indexer.svg?2)](https://pypi.org/project/mcp-code-indexer/)
60
+ [![PyPI version](https://badge.fury.io/py/mcp-code-indexer.svg?4)](https://badge.fury.io/py/mcp-code-indexer)
61
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-code-indexer.svg?4)](https://pypi.org/project/mcp-code-indexer/)
62
62
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
63
63
 
64
64
  A production-ready **Model Context Protocol (MCP) server** that revolutionizes how AI agents navigate and understand codebases. Instead of repeatedly scanning files, agents get instant access to intelligent descriptions, semantic search, and context-aware recommendations.
@@ -246,6 +246,28 @@ The server provides **11 powerful MCP tools** for intelligent codebase managemen
246
246
 
247
247
  💡 **Pro Tip**: Always start with `check_codebase_size` to get personalized recommendations for navigating your specific codebase.
248
248
 
249
+ ## 🔗 Git Hook Integration
250
+
251
+ Keep your codebase documentation automatically synchronized with automated analysis on every commit, rebase, or merge:
252
+
253
+ ```bash
254
+ # Analyze current staged changes
255
+ mcp-code-indexer --githook
256
+
257
+ # Analyze a specific commit
258
+ mcp-code-indexer --githook abc123def
259
+
260
+ # Analyze a commit range (perfect for rebases)
261
+ mcp-code-indexer --githook abc123 def456
262
+ ```
263
+
264
+ **🎯 Perfect for**:
265
+ - **Automated documentation** that never goes stale
266
+ - **Rebase-aware analysis** that handles complex git operations
267
+ - **Zero-effort maintenance** with background processing
268
+
269
+ See the **[Git Hook Setup Guide](docs/git-hook-setup.md)** for complete installation instructions including post-commit, post-merge, and post-rewrite hooks.
270
+
249
271
  ## 🏗️ Architecture Highlights
250
272
 
251
273
  ### Performance Optimized