Skip to content

Commit be85d37

Browse files
authored
feat(github): add required_signatures branch-protection tools (#178) (#180)
GitHub exposes commit-signature enforcement on a separate endpoint (.../protection/required_signatures), so it could not be set through github_update_branch_protection. Closes that gap by adding a dedicated enable/disable/get trio, mirroring the existing github_*_vulnerability_alerts pattern. New tools: - github_get_required_signatures(repo_owner, repo_name, branch) - github_enable_required_signatures(repo_owner, repo_name, branch) - github_disable_required_signatures(repo_owner, repo_name, branch) Endpoint: PUT/DELETE/GET /repos/{owner}/{repo}/branches/{branch}/protection/required_signatures - security.py: three async functions using github_client_context(). PUT returns 200+JSON (not 204 like vulnerability_alerts PUT); DELETE returns 204; GET returns 200 with {enabled, url} or 404 when branch protection is absent. - models.py: three Pydantic models with (repo_owner, repo_name, branch). - registry_github.py: ToolDefinition trio added after vulnerability_alerts. - api.py untouched (wildcard re-export of security.py covers new funcs). - README.md tool counts: 52 -> 55 and 51/22 -> 54/25. - tests/unit/github/test_github_required_signatures.py: 7 tests via AsyncMock+patch(github_client_context) covering get (enabled/disabled/ 404), enable (200/404), disable (204/404). Closes #178
1 parent eb4d785 commit be85d37

5 files changed

Lines changed: 387 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,15 @@ The server provides Azure DevOps integration for monitoring and analyzing Azure
137137

138138
### Lean MCP Interface (Context-Optimized)
139139

140-
The server provides an alternative **lean interface** that reduces context consumption by ~97% (from ~30k tokens to ~900 tokens). Instead of exposing all 52 tools upfront, it uses a 3-meta-tool pattern:
140+
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:
141141

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

148-
**Tool Coverage**: All 51 tools remain accessible (26 git, 22 GitHub, 3 Azure DevOps).
148+
**Tool Coverage**: All 54 tools remain accessible (26 git, 25 GitHub, 3 Azure DevOps).
149149

150150
**Usage Example**:
151151
```python

src/mcp_server_git/github/models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,30 @@ class GitHubDisableVulnerabilityAlerts(BaseModel):
670670
repo_name: str
671671

672672

673+
class GitHubGetRequiredSignatures(BaseModel):
674+
"""Model for checking if required signatures are enabled on a protected branch."""
675+
676+
repo_owner: str
677+
repo_name: str
678+
branch: str
679+
680+
681+
class GitHubEnableRequiredSignatures(BaseModel):
682+
"""Model for enabling required signatures on a protected branch."""
683+
684+
repo_owner: str
685+
repo_name: str
686+
branch: str
687+
688+
689+
class GitHubDisableRequiredSignatures(BaseModel):
690+
"""Model for disabling required signatures on a protected branch."""
691+
692+
repo_owner: str
693+
repo_name: str
694+
branch: str
695+
696+
673697
class GitHubGetAutomatedSecurityFixes(BaseModel):
674698
"""Model for checking if automated security fixes are enabled."""
675699

src/mcp_server_git/github/security.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""GitHub security operations - vulnerability alerts, security fixes, analysis."""
2+
23
from __future__ import annotations
34
import logging
45
from mcp_server_git.github.client import github_client_context
6+
57
logger = logging.getLogger(__name__)
68

79

@@ -115,6 +117,123 @@ async def github_disable_vulnerability_alerts(
115117
return f"❌ Error disabling vulnerability alerts: {str(e)}"
116118

117119

120+
async def github_get_required_signatures(
121+
repo_owner: str,
122+
repo_name: str,
123+
branch: str,
124+
) -> str:
125+
"""Check if required signatures are enabled on a protected branch."""
126+
logger.debug(
127+
f"🔍 Getting required signatures status for {repo_owner}/{repo_name}#{branch}"
128+
)
129+
130+
try:
131+
async with github_client_context() as client:
132+
response = await client.get(
133+
f"/repos/{repo_owner}/{repo_name}/branches/{branch}/protection/required_signatures"
134+
)
135+
136+
if response.status == 200:
137+
data = await response.json()
138+
enabled = data.get("enabled", False)
139+
return f"{'✅' if enabled else '❌'} Required signatures: {'enabled' if enabled else 'disabled'} on {repo_owner}/{repo_name}#{branch}"
140+
elif response.status == 404:
141+
return f"❌ Branch protection or branch not found: {repo_owner}/{repo_name}#{branch}"
142+
else:
143+
error_text = await response.text()
144+
return f"❌ Failed to check required signatures: {response.status} - {error_text}"
145+
146+
except ValueError as auth_error:
147+
logger.error(f"Authentication error checking required signatures: {auth_error}")
148+
return f"❌ {str(auth_error)}"
149+
except ConnectionError as conn_error:
150+
logger.error(f"Connection error checking required signatures: {conn_error}")
151+
return f"❌ Network connection failed: {str(conn_error)}"
152+
except Exception as e:
153+
logger.error(
154+
f"Unexpected error checking required signatures: {e}", exc_info=True
155+
)
156+
return f"❌ Error checking required signatures: {str(e)}"
157+
158+
159+
async def github_enable_required_signatures(
160+
repo_owner: str,
161+
repo_name: str,
162+
branch: str,
163+
) -> str:
164+
"""Enable required signatures on a protected branch."""
165+
logger.debug(
166+
f"🚀 Enabling required signatures for {repo_owner}/{repo_name}#{branch}"
167+
)
168+
169+
try:
170+
async with github_client_context() as client:
171+
response = await client.put(
172+
f"/repos/{repo_owner}/{repo_name}/branches/{branch}/protection/required_signatures"
173+
)
174+
175+
if response.status == 200:
176+
logger.info(
177+
f"✅ Enabled required signatures for {repo_owner}/{repo_name}#{branch}"
178+
)
179+
return f"✅ Enabled required signatures on {repo_owner}/{repo_name}#{branch}"
180+
else:
181+
error_text = await response.text()
182+
return f"❌ Failed to enable required signatures: {response.status} - {error_text}"
183+
184+
except ValueError as auth_error:
185+
logger.error(f"Authentication error enabling required signatures: {auth_error}")
186+
return f"❌ {str(auth_error)}"
187+
except ConnectionError as conn_error:
188+
logger.error(f"Connection error enabling required signatures: {conn_error}")
189+
return f"❌ Network connection failed: {str(conn_error)}"
190+
except Exception as e:
191+
logger.error(
192+
f"Unexpected error enabling required signatures: {e}", exc_info=True
193+
)
194+
return f"❌ Error enabling required signatures: {str(e)}"
195+
196+
197+
async def github_disable_required_signatures(
198+
repo_owner: str,
199+
repo_name: str,
200+
branch: str,
201+
) -> str:
202+
"""Disable required signatures on a protected branch."""
203+
logger.debug(
204+
f"🚀 Disabling required signatures for {repo_owner}/{repo_name}#{branch}"
205+
)
206+
207+
try:
208+
async with github_client_context() as client:
209+
response = await client.delete(
210+
f"/repos/{repo_owner}/{repo_name}/branches/{branch}/protection/required_signatures"
211+
)
212+
213+
if response.status == 204:
214+
logger.info(
215+
f"✅ Disabled required signatures for {repo_owner}/{repo_name}#{branch}"
216+
)
217+
return f"✅ Disabled required signatures on {repo_owner}/{repo_name}#{branch}"
218+
else:
219+
error_text = await response.text()
220+
return f"❌ Failed to disable required signatures: {response.status} - {error_text}"
221+
222+
except ValueError as auth_error:
223+
logger.error(
224+
f"Authentication error disabling required signatures: {auth_error}"
225+
)
226+
return f"❌ {str(auth_error)}"
227+
except ConnectionError as conn_error:
228+
logger.error(f"Connection error disabling required signatures: {conn_error}")
229+
return f"❌ Network connection failed: {str(conn_error)}"
230+
except Exception as e:
231+
logger.error(
232+
f"Unexpected error disabling required signatures: {e}", exc_info=True
233+
)
234+
return f"❌ Error disabling required signatures: {str(e)}"
235+
236+
118237
async def github_get_automated_security_fixes(
119238
repo_owner: str,
120239
repo_name: str,

src/mcp_server_git/lean/registry_github.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
GitHubDeleteRelease,
1515
GitHubDeleteReleaseAsset,
1616
GitHubDisableAutomatedSecurityFixes,
17+
GitHubDisableRequiredSignatures,
1718
GitHubDisableVulnerabilityAlerts,
1819
GitHubEditPRDescription,
1920
GitHubEnableAutomatedSecurityFixes,
21+
GitHubEnableRequiredSignatures,
2022
GitHubEnableVulnerabilityAlerts,
2123
GitHubGetActionsPermissions,
2224
GitHubGetAutomatedSecurityFixes,
@@ -29,6 +31,7 @@
2931
GitHubGetPRFiles,
3032
GitHubGetPRStatus,
3133
GitHubGetRelease,
34+
GitHubGetRequiredSignatures,
3235
GitHubGetRepoSettings,
3336
GitHubGetSecurityAnalysis,
3437
GitHubGetVulnerabilityAlerts,
@@ -450,6 +453,30 @@ def _register_github_tools(interface: Any, github_service: Any):
450453
domain="github",
451454
complexity="focused",
452455
),
456+
ToolDefinition(
457+
name="github_get_required_signatures",
458+
implementation=github_ops.github_get_required_signatures,
459+
description="Get required-signatures status for a protected branch",
460+
schema=GitHubGetRequiredSignatures.model_json_schema(),
461+
domain="github",
462+
complexity="core",
463+
),
464+
ToolDefinition(
465+
name="github_enable_required_signatures",
466+
implementation=github_ops.github_enable_required_signatures,
467+
description="Enable required signatures on a protected branch",
468+
schema=GitHubEnableRequiredSignatures.model_json_schema(),
469+
domain="github",
470+
complexity="focused",
471+
),
472+
ToolDefinition(
473+
name="github_disable_required_signatures",
474+
implementation=github_ops.github_disable_required_signatures,
475+
description="Disable required signatures on a protected branch",
476+
schema=GitHubDisableRequiredSignatures.model_json_schema(),
477+
domain="github",
478+
complexity="focused",
479+
),
453480
ToolDefinition(
454481
name="github_get_automated_security_fixes",
455482
implementation=github_ops.github_get_automated_security_fixes,

0 commit comments

Comments
 (0)