Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,15 @@ The server provides Azure DevOps integration for monitoring and analyzing Azure

### Lean MCP Interface (Context-Optimized)

The server provides an alternative **lean interface** that reduces context consumption by ~97% (from ~30k tokens to ~900 tokens). Instead of exposing all 55 tools upfront, it uses a 3-meta-tool pattern:
The server provides an alternative **lean interface** that reduces context consumption by ~97% (from ~30k tokens to ~900 tokens). Instead of exposing all 56 tools upfront, it uses a 3-meta-tool pattern:

| Meta-Tool | Purpose |
|-----------|---------|
| `discover_tools(pattern)` | List available tools with optional filtering |
| `get_tool_spec(tool_name)` | Get full schema for a specific tool on-demand |
| `execute_tool(tool_name, params)` | Execute any tool dynamically |

**Tool Coverage**: All 54 tools remain accessible (26 git, 25 GitHub, 3 Azure DevOps).
**Tool Coverage**: All 55 tools remain accessible (27 git, 25 GitHub, 3 Azure DevOps).

**Usage Example**:
```python
Expand Down
103 changes: 103 additions & 0 deletions src/mcp_server_git/git/_commit_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
import os
import re
import subprocess

from ..utils.git_import import GitCommandError, Repo
Expand All @@ -14,6 +15,7 @@
"git_log",
"git_show",
"git_blame",
"git_reflog",
]


Expand Down Expand Up @@ -281,3 +283,104 @@ def git_blame(
return f"❌ Blame failed: {str(e)}"
except Exception as e:
return f"❌ Blame error: {str(e)}"


def git_reflog(
repo: Repo,
ref: str = "HEAD",
max_count: int | None = None,
all: bool = False, # noqa: A002
) -> list[dict] | str:
"""Read git reflog (HEAD/ref movement history including non-commit operations).

Args:
repo: Git repository object
ref: Ref to show reflog for (default: HEAD)
max_count: Maximum number of entries to return
all: If True, show reflog for all refs (--all)

Returns:
List of dicts with keys: new_sha, label, action, message, and
old_sha (the SHA before this operation, derived from the next entry's
new_sha since reflog is newest-first; absent for the oldest entry).
Returns a string starting with '❌' on error.

Note on old_sha:
%gP (reflog parent selector) yields a reflog selector like 'HEAD@{1}',
not a commit SHA. old_sha is instead taken from entry[i+1].new_sha,
which is always the commit HEAD pointed to before entry[i]'s operation.
"""
try:
# %H = new (post-move) full SHA, %gD = reflog selector (HEAD@{0}),
# %gs = reflog subject (e.g. "checkout: moving from main to feature")
# Note: %gP gives a reflog selector (HEAD@{N}), not a commit SHA,
# so old_sha is derived from the next entry's new_sha instead.
sep = "\x00"
fmt = f"%H{sep}%gD{sep}%gs"

args = ["show", f"--format={fmt}", "--no-abbrev", "--no-patch"]

# max_count <= 0 (or None) means no limit; only positive values constrain.
if max_count is not None and max_count > 0:
args.extend(["-n", str(max_count)])

if all: # noqa: A002
args.append("--all")
else:
args.append(ref)

raw_output = repo.git.reflog(*args)

if not raw_output.strip():
return []

entries = []
for line in raw_output.splitlines():
line = line.strip()
if not line:
continue
parts = line.split(sep)
if len(parts) < 3:
continue
new_sha, label, subject = parts[0], parts[1], parts[2]

# Derive action from subject: verb before ':', keeping any
# parenthetical qualifier that immediately follows the verb.
# Examples:
# "checkout: moving from A to B" -> "checkout"
# "merge origin/main: Fast-forward" -> "merge"
# "commit (amend): fixup msg" -> "commit (amend)"
# "rebase (start): checkout main" -> "rebase (start)"
# "commit (initial import): msg" -> "commit (initial import)"
pre_colon = subject.split(":", 1)[0].strip() if subject else ""
m = re.match(r'^(\w[\w-]*(?:\s+\([^)]*\))?)', pre_colon)
action = m.group(1) if m else (pre_colon.split()[0] if pre_colon.split() else "unknown")

entries.append({
"new_sha": new_sha,
"label": label,
"action": action,
"message": subject,
})

# old_sha for entry[i] = entry[i+1].new_sha (reflog is newest-first).
for i in range(len(entries) - 1):
entries[i]["old_sha"] = entries[i + 1]["new_sha"]

# Warn on large output, mirroring git_show's 50KB threshold.
serialized_size = len(str(entries))
if serialized_size > 50000: # 50KB threshold
entries.append({
"warning": (
f"⚠️ Large reflog detected ({len(entries) - 1} entries, "
f"~{serialized_size // 1000}KB). "
"Consider using max_count to limit output."
)
})

return entries

except GitCommandError as e:
return f"❌ Reflog failed: {str(e)}"
except Exception as e:
return f"❌ Reflog error: {str(e)}"
10 changes: 10 additions & 0 deletions src/mcp_server_git/git/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ class GitLog(BaseModel):
merges: bool | None = None # None=all, True=only merges, False=no merges


class GitReflog(BaseModel):
repo_path: str
ref: str = Field(default="HEAD", description="Ref to show reflog for (default: HEAD)")
max_count: int | None = Field(default=None, ge=0, description="Maximum number of reflog entries to return")
all: bool = Field( # noqa: A003
default=False,
description="Show reflog for all refs (git reflog --all)",
)


class GitCreateBranch(BaseModel):
repo_path: str
branch_name: str
Expand Down
2 changes: 2 additions & 0 deletions src/mcp_server_git/git/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
git_blame,
git_commit,
git_log,
git_reflog,
git_show,
git_status,
)
Expand Down Expand Up @@ -105,6 +106,7 @@
"git_tag_create",
"git_tag_delete",
"git_blame",
"git_reflog",
"git_branch_list",
"git_merge_base",
"git_submodule_status",
Expand Down
9 changes: 9 additions & 0 deletions src/mcp_server_git/lean/registry_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
GitDiffUnstaged,
GitInit,
GitLog,
GitReflog,
GitMerge,
GitMergeBase,
GitMergeTree,
Expand Down Expand Up @@ -139,6 +140,14 @@ def wrapper(repo_path: str, **kwargs):
domain="git",
complexity="core",
),
ToolDefinition(
name="git_reflog",
implementation=wrap_repo_op(git_ops.git_reflog),
description="Read git reflog (HEAD/ref movement history incl. non-commit ops)",
schema=GitReflog.model_json_schema(),
domain="git",
complexity="core",
),
ToolDefinition(
name="git_create_branch",
implementation=wrap_repo_op(git_ops.git_create_branch),
Expand Down
Loading
Loading