commit-check-mcp 0.1.1.post1.dev1__py3-none-any.whl
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.
- commit_check_mcp/__init__.py +6 -0
- commit_check_mcp/server.py +504 -0
- commit_check_mcp-0.1.1.post1.dev1.dist-info/METADATA +86 -0
- commit_check_mcp-0.1.1.post1.dev1.dist-info/RECORD +8 -0
- commit_check_mcp-0.1.1.post1.dev1.dist-info/WHEEL +5 -0
- commit_check_mcp-0.1.1.post1.dev1.dist-info/entry_points.txt +2 -0
- commit_check_mcp-0.1.1.post1.dev1.dist-info/licenses/LICENSE +21 -0
- commit_check_mcp-0.1.1.post1.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"""MCP server for commit-check validations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
import io
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from commit_check import __version__ as commit_check_version
|
|
14
|
+
from commit_check.config_merger import deep_merge, get_default_config, load_toml_config
|
|
15
|
+
from commit_check.engine import ValidationContext, ValidationEngine, ValidationResult
|
|
16
|
+
from commit_check.rule_builder import RuleBuilder, ValidationRule
|
|
17
|
+
from commit_check.rules_catalog import BRANCH_RULES, COMMIT_RULES
|
|
18
|
+
from mcp.server.fastmcp import FastMCP
|
|
19
|
+
|
|
20
|
+
from . import __version__
|
|
21
|
+
|
|
22
|
+
mcp = FastMCP(
|
|
23
|
+
"commit-check-mcp",
|
|
24
|
+
instructions=(
|
|
25
|
+
"Use these tools to validate commit messages, branch names, and author metadata "
|
|
26
|
+
"with commit-check."
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalize_config(config: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
32
|
+
"""Ensure tool config input is JSON-object-like."""
|
|
33
|
+
if config is None:
|
|
34
|
+
return None
|
|
35
|
+
if not isinstance(config, dict):
|
|
36
|
+
raise ValueError("config must be an object/dictionary when provided")
|
|
37
|
+
return config
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize_repo_path(repo_path: str | None) -> Path | None:
|
|
41
|
+
"""Normalize and validate an optional repository path."""
|
|
42
|
+
if repo_path is None:
|
|
43
|
+
return None
|
|
44
|
+
if not isinstance(repo_path, str):
|
|
45
|
+
raise ValueError("repo_path must be a string when provided")
|
|
46
|
+
normalized = repo_path.strip()
|
|
47
|
+
if not normalized:
|
|
48
|
+
raise ValueError("repo_path cannot be empty when provided")
|
|
49
|
+
|
|
50
|
+
path = Path(normalized).expanduser().resolve()
|
|
51
|
+
if not path.exists():
|
|
52
|
+
raise ValueError(f"repo_path does not exist: {path}")
|
|
53
|
+
if not path.is_dir():
|
|
54
|
+
raise ValueError(f"repo_path must be a directory: {path}")
|
|
55
|
+
return path
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _normalize_config_path(config_path: str | None, repo_path: Path | None) -> str | None:
|
|
59
|
+
"""Normalize and validate an optional config path."""
|
|
60
|
+
if config_path is None:
|
|
61
|
+
return None
|
|
62
|
+
if not isinstance(config_path, str):
|
|
63
|
+
raise ValueError("config_path must be a string when provided")
|
|
64
|
+
normalized = config_path.strip()
|
|
65
|
+
if not normalized:
|
|
66
|
+
raise ValueError("config_path cannot be empty when provided")
|
|
67
|
+
|
|
68
|
+
path = Path(normalized).expanduser()
|
|
69
|
+
if not path.is_absolute() and repo_path is not None:
|
|
70
|
+
path = repo_path / path
|
|
71
|
+
|
|
72
|
+
resolved = path.resolve()
|
|
73
|
+
if not resolved.exists():
|
|
74
|
+
raise ValueError(f"config_path does not exist: {resolved}")
|
|
75
|
+
if not resolved.is_file():
|
|
76
|
+
raise ValueError(f"config_path must be a file: {resolved}")
|
|
77
|
+
return str(resolved)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@contextmanager
|
|
81
|
+
def _working_directory(repo_path: Path | None):
|
|
82
|
+
"""Temporarily switch working directory for repo-relative config and git checks."""
|
|
83
|
+
if repo_path is None:
|
|
84
|
+
yield
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
original_cwd = Path.cwd()
|
|
88
|
+
os.chdir(repo_path)
|
|
89
|
+
try:
|
|
90
|
+
yield
|
|
91
|
+
finally:
|
|
92
|
+
os.chdir(original_cwd)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _merge_config(
|
|
96
|
+
config: dict[str, Any] | None,
|
|
97
|
+
*,
|
|
98
|
+
repo_path: Path | None = None,
|
|
99
|
+
config_path: str | None = None,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""Merge repository config and user config on top of commit-check defaults."""
|
|
102
|
+
merged = get_default_config()
|
|
103
|
+
with _working_directory(repo_path):
|
|
104
|
+
loaded_config = load_toml_config(config_path or "")
|
|
105
|
+
if loaded_config:
|
|
106
|
+
deep_merge(merged, loaded_config)
|
|
107
|
+
if config:
|
|
108
|
+
deep_merge(merged, config)
|
|
109
|
+
return merged
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _run_checks(
|
|
113
|
+
check_names: list[str],
|
|
114
|
+
context: ValidationContext,
|
|
115
|
+
config: dict[str, Any],
|
|
116
|
+
) -> dict[str, Any]:
|
|
117
|
+
"""Run commit-check rules and always return structured per-check results."""
|
|
118
|
+
rules = RuleBuilder(config).build_all_rules()
|
|
119
|
+
filtered: list[ValidationRule] = [r for r in rules if r.check in check_names]
|
|
120
|
+
|
|
121
|
+
checks: list[dict[str, Any]] = []
|
|
122
|
+
for rule in filtered:
|
|
123
|
+
with io.StringIO() as _out, io.StringIO() as _err:
|
|
124
|
+
with redirect_stdout(_out), redirect_stderr(_err):
|
|
125
|
+
status = ValidationEngine([rule]).validate_all(context)
|
|
126
|
+
passed = status == ValidationResult.PASS
|
|
127
|
+
checks.append(
|
|
128
|
+
{
|
|
129
|
+
"check": rule.check,
|
|
130
|
+
"status": "pass" if passed else "fail",
|
|
131
|
+
"value": context.stdin_text or "",
|
|
132
|
+
"error": "" if passed else (rule.error or ""),
|
|
133
|
+
"suggest": "" if passed else (rule.suggest or ""),
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
overall = "fail" if any(c["status"] == "fail" for c in checks) else "pass"
|
|
138
|
+
return {"status": overall, "checks": checks}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _validate_message(
|
|
142
|
+
message: str,
|
|
143
|
+
*,
|
|
144
|
+
config: dict[str, Any] | None = None,
|
|
145
|
+
repo_path: Path | None = None,
|
|
146
|
+
config_path: str | None = None,
|
|
147
|
+
) -> dict[str, Any]:
|
|
148
|
+
"""Validate message using commit-check engine internals."""
|
|
149
|
+
cfg = _merge_config(config, repo_path=repo_path, config_path=config_path)
|
|
150
|
+
with _working_directory(repo_path):
|
|
151
|
+
return _run_checks(
|
|
152
|
+
[
|
|
153
|
+
"message",
|
|
154
|
+
"subject_imperative",
|
|
155
|
+
"subject_max_length",
|
|
156
|
+
"subject_min_length",
|
|
157
|
+
"subject_capitalized",
|
|
158
|
+
"require_signed_off_by",
|
|
159
|
+
"require_body",
|
|
160
|
+
"allow_merge_commits",
|
|
161
|
+
"allow_revert_commits",
|
|
162
|
+
"allow_empty_commits",
|
|
163
|
+
"allow_fixup_commits",
|
|
164
|
+
"allow_wip_commits",
|
|
165
|
+
],
|
|
166
|
+
ValidationContext(stdin_text=message, config=cfg),
|
|
167
|
+
cfg,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _validate_branch(
|
|
172
|
+
branch: str | None = None,
|
|
173
|
+
*,
|
|
174
|
+
config: dict[str, Any] | None = None,
|
|
175
|
+
repo_path: Path | None = None,
|
|
176
|
+
config_path: str | None = None,
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
"""Validate branch name using commit-check engine internals."""
|
|
179
|
+
cfg = _merge_config(config, repo_path=repo_path, config_path=config_path)
|
|
180
|
+
with _working_directory(repo_path):
|
|
181
|
+
return _run_checks(
|
|
182
|
+
["branch", "merge_base"],
|
|
183
|
+
ValidationContext(stdin_text=branch, config=cfg),
|
|
184
|
+
cfg,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _validate_author(
|
|
189
|
+
name: str | None = None,
|
|
190
|
+
email: str | None = None,
|
|
191
|
+
*,
|
|
192
|
+
config: dict[str, Any] | None = None,
|
|
193
|
+
repo_path: Path | None = None,
|
|
194
|
+
config_path: str | None = None,
|
|
195
|
+
) -> dict[str, Any]:
|
|
196
|
+
"""Validate author info using commit-check engine internals."""
|
|
197
|
+
cfg = _merge_config(config, repo_path=repo_path, config_path=config_path)
|
|
198
|
+
|
|
199
|
+
with _working_directory(repo_path):
|
|
200
|
+
if name is not None and email is not None:
|
|
201
|
+
name_result = _run_checks(
|
|
202
|
+
["author_name"],
|
|
203
|
+
ValidationContext(stdin_text=name, config=cfg),
|
|
204
|
+
cfg,
|
|
205
|
+
)
|
|
206
|
+
email_result = _run_checks(
|
|
207
|
+
["author_email"],
|
|
208
|
+
ValidationContext(stdin_text=email, config=cfg),
|
|
209
|
+
cfg,
|
|
210
|
+
)
|
|
211
|
+
checks = name_result["checks"] + email_result["checks"]
|
|
212
|
+
return {
|
|
213
|
+
"status": "fail" if any(c["status"] == "fail" for c in checks) else "pass",
|
|
214
|
+
"checks": checks,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
check_names: list[str] = []
|
|
218
|
+
stdin = None
|
|
219
|
+
if name is not None:
|
|
220
|
+
check_names.append("author_name")
|
|
221
|
+
stdin = name
|
|
222
|
+
if email is not None:
|
|
223
|
+
check_names.append("author_email")
|
|
224
|
+
stdin = email
|
|
225
|
+
if not check_names:
|
|
226
|
+
check_names = ["author_name", "author_email"]
|
|
227
|
+
|
|
228
|
+
return _run_checks(check_names, ValidationContext(stdin_text=stdin, config=cfg), cfg)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _validate_all(
|
|
232
|
+
message: str | None = None,
|
|
233
|
+
branch: str | None = None,
|
|
234
|
+
author_name: str | None = None,
|
|
235
|
+
author_email: str | None = None,
|
|
236
|
+
*,
|
|
237
|
+
config: dict[str, Any] | None = None,
|
|
238
|
+
repo_path: Path | None = None,
|
|
239
|
+
config_path: str | None = None,
|
|
240
|
+
) -> dict[str, Any]:
|
|
241
|
+
"""Validate multiple commit-check contexts and combine outcomes."""
|
|
242
|
+
cfg = _merge_config(config, repo_path=repo_path, config_path=config_path)
|
|
243
|
+
checks: list[dict[str, Any]] = []
|
|
244
|
+
|
|
245
|
+
with _working_directory(repo_path):
|
|
246
|
+
if message is not None:
|
|
247
|
+
checks.extend(
|
|
248
|
+
_run_checks(
|
|
249
|
+
[
|
|
250
|
+
"message",
|
|
251
|
+
"subject_imperative",
|
|
252
|
+
"subject_max_length",
|
|
253
|
+
"subject_min_length",
|
|
254
|
+
"subject_capitalized",
|
|
255
|
+
"require_signed_off_by",
|
|
256
|
+
"require_body",
|
|
257
|
+
"allow_merge_commits",
|
|
258
|
+
"allow_revert_commits",
|
|
259
|
+
"allow_empty_commits",
|
|
260
|
+
"allow_fixup_commits",
|
|
261
|
+
"allow_wip_commits",
|
|
262
|
+
],
|
|
263
|
+
ValidationContext(stdin_text=message, config=cfg),
|
|
264
|
+
cfg,
|
|
265
|
+
)["checks"]
|
|
266
|
+
)
|
|
267
|
+
if branch is not None:
|
|
268
|
+
checks.extend(
|
|
269
|
+
_run_checks(
|
|
270
|
+
["branch", "merge_base"],
|
|
271
|
+
ValidationContext(stdin_text=branch, config=cfg),
|
|
272
|
+
cfg,
|
|
273
|
+
)["checks"]
|
|
274
|
+
)
|
|
275
|
+
if author_name is not None or author_email is not None:
|
|
276
|
+
if author_name is not None:
|
|
277
|
+
checks.extend(
|
|
278
|
+
_run_checks(
|
|
279
|
+
["author_name"],
|
|
280
|
+
ValidationContext(stdin_text=author_name, config=cfg),
|
|
281
|
+
cfg,
|
|
282
|
+
)["checks"]
|
|
283
|
+
)
|
|
284
|
+
if author_email is not None:
|
|
285
|
+
checks.extend(
|
|
286
|
+
_run_checks(
|
|
287
|
+
["author_email"],
|
|
288
|
+
ValidationContext(stdin_text=author_email, config=cfg),
|
|
289
|
+
cfg,
|
|
290
|
+
)["checks"]
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
"status": "fail" if any(c["status"] == "fail" for c in checks) else "pass",
|
|
295
|
+
"checks": checks,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@mcp.tool()
|
|
300
|
+
def server_health() -> dict[str, str]:
|
|
301
|
+
"""Return server and dependency versions."""
|
|
302
|
+
return {
|
|
303
|
+
"server": "commit-check-mcp",
|
|
304
|
+
"server_version": __version__,
|
|
305
|
+
"commit_check_version": commit_check_version,
|
|
306
|
+
"mcp_sdk_version": version("mcp"),
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@mcp.tool()
|
|
311
|
+
def validate_commit_message(
|
|
312
|
+
message: str,
|
|
313
|
+
config: dict[str, Any] | None = None,
|
|
314
|
+
repo_path: str | None = None,
|
|
315
|
+
config_path: str | None = None,
|
|
316
|
+
) -> dict[str, Any]:
|
|
317
|
+
"""Validate a commit message against commit-check rules."""
|
|
318
|
+
if not isinstance(message, str) or not message.strip():
|
|
319
|
+
raise ValueError("message must be a non-empty string")
|
|
320
|
+
normalized_repo_path = _normalize_repo_path(repo_path)
|
|
321
|
+
return _validate_message(
|
|
322
|
+
message.strip(),
|
|
323
|
+
config=_normalize_config(config),
|
|
324
|
+
repo_path=normalized_repo_path,
|
|
325
|
+
config_path=_normalize_config_path(config_path, normalized_repo_path),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@mcp.tool()
|
|
330
|
+
def validate_branch_name(
|
|
331
|
+
branch: str | None = None,
|
|
332
|
+
config: dict[str, Any] | None = None,
|
|
333
|
+
repo_path: str | None = None,
|
|
334
|
+
config_path: str | None = None,
|
|
335
|
+
) -> dict[str, Any]:
|
|
336
|
+
"""Validate branch naming conventions with commit-check."""
|
|
337
|
+
normalized_branch = branch.strip() if isinstance(branch, str) else None
|
|
338
|
+
if isinstance(branch, str) and not normalized_branch:
|
|
339
|
+
raise ValueError("branch cannot be empty when provided")
|
|
340
|
+
normalized_repo_path = _normalize_repo_path(repo_path)
|
|
341
|
+
return _validate_branch(
|
|
342
|
+
normalized_branch,
|
|
343
|
+
config=_normalize_config(config),
|
|
344
|
+
repo_path=normalized_repo_path,
|
|
345
|
+
config_path=_normalize_config_path(config_path, normalized_repo_path),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@mcp.tool()
|
|
350
|
+
def validate_author_info(
|
|
351
|
+
author_name: str | None = None,
|
|
352
|
+
author_email: str | None = None,
|
|
353
|
+
config: dict[str, Any] | None = None,
|
|
354
|
+
repo_path: str | None = None,
|
|
355
|
+
config_path: str | None = None,
|
|
356
|
+
) -> dict[str, Any]:
|
|
357
|
+
"""Validate commit author name and/or email with commit-check."""
|
|
358
|
+
normalized_name = author_name.strip() if isinstance(author_name, str) else None
|
|
359
|
+
normalized_email = author_email.strip() if isinstance(author_email, str) else None
|
|
360
|
+
|
|
361
|
+
if isinstance(author_name, str) and not normalized_name:
|
|
362
|
+
raise ValueError("author_name cannot be empty when provided")
|
|
363
|
+
if isinstance(author_email, str) and not normalized_email:
|
|
364
|
+
raise ValueError("author_email cannot be empty when provided")
|
|
365
|
+
normalized_repo_path = _normalize_repo_path(repo_path)
|
|
366
|
+
|
|
367
|
+
return _validate_author(
|
|
368
|
+
normalized_name,
|
|
369
|
+
normalized_email,
|
|
370
|
+
config=_normalize_config(config),
|
|
371
|
+
repo_path=normalized_repo_path,
|
|
372
|
+
config_path=_normalize_config_path(config_path, normalized_repo_path),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@mcp.tool()
|
|
377
|
+
def validate_commit_context(
|
|
378
|
+
message: str | None = None,
|
|
379
|
+
branch: str | None = None,
|
|
380
|
+
author_name: str | None = None,
|
|
381
|
+
author_email: str | None = None,
|
|
382
|
+
config: dict[str, Any] | None = None,
|
|
383
|
+
repo_path: str | None = None,
|
|
384
|
+
config_path: str | None = None,
|
|
385
|
+
) -> dict[str, Any]:
|
|
386
|
+
"""Run combined commit-check validations in one call."""
|
|
387
|
+
normalized_message = message.strip() if isinstance(message, str) else None
|
|
388
|
+
normalized_branch = branch.strip() if isinstance(branch, str) else None
|
|
389
|
+
normalized_name = author_name.strip() if isinstance(author_name, str) else None
|
|
390
|
+
normalized_email = author_email.strip() if isinstance(author_email, str) else None
|
|
391
|
+
|
|
392
|
+
if isinstance(message, str) and not normalized_message:
|
|
393
|
+
raise ValueError("message cannot be empty when provided")
|
|
394
|
+
if isinstance(branch, str) and not normalized_branch:
|
|
395
|
+
raise ValueError("branch cannot be empty when provided")
|
|
396
|
+
if isinstance(author_name, str) and not normalized_name:
|
|
397
|
+
raise ValueError("author_name cannot be empty when provided")
|
|
398
|
+
if isinstance(author_email, str) and not normalized_email:
|
|
399
|
+
raise ValueError("author_email cannot be empty when provided")
|
|
400
|
+
|
|
401
|
+
if not any([normalized_message, normalized_branch, normalized_name, normalized_email]):
|
|
402
|
+
raise ValueError(
|
|
403
|
+
"At least one of message, branch, author_name, or author_email must be provided"
|
|
404
|
+
)
|
|
405
|
+
normalized_repo_path = _normalize_repo_path(repo_path)
|
|
406
|
+
|
|
407
|
+
return _validate_all(
|
|
408
|
+
message=normalized_message,
|
|
409
|
+
branch=normalized_branch,
|
|
410
|
+
author_name=normalized_name,
|
|
411
|
+
author_email=normalized_email,
|
|
412
|
+
config=_normalize_config(config),
|
|
413
|
+
repo_path=normalized_repo_path,
|
|
414
|
+
config_path=_normalize_config_path(config_path, normalized_repo_path),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@mcp.tool()
|
|
419
|
+
def validate_repository_state(
|
|
420
|
+
repo_path: str | None = None,
|
|
421
|
+
config: dict[str, Any] | None = None,
|
|
422
|
+
config_path: str | None = None,
|
|
423
|
+
include_message: bool = True,
|
|
424
|
+
include_branch: bool = True,
|
|
425
|
+
include_author: bool = True,
|
|
426
|
+
) -> dict[str, Any]:
|
|
427
|
+
"""Validate the latest commit, current branch, and git author state of a repository."""
|
|
428
|
+
if not any([include_message, include_branch, include_author]):
|
|
429
|
+
raise ValueError("At least one validation target must be enabled")
|
|
430
|
+
|
|
431
|
+
normalized_repo_path = _normalize_repo_path(repo_path)
|
|
432
|
+
normalized_config = _normalize_config(config)
|
|
433
|
+
normalized_config_path = _normalize_config_path(config_path, normalized_repo_path)
|
|
434
|
+
|
|
435
|
+
checks: list[dict[str, Any]] = []
|
|
436
|
+
if include_message:
|
|
437
|
+
checks.extend(
|
|
438
|
+
_validate_message(
|
|
439
|
+
"",
|
|
440
|
+
config=normalized_config,
|
|
441
|
+
repo_path=normalized_repo_path,
|
|
442
|
+
config_path=normalized_config_path,
|
|
443
|
+
)["checks"]
|
|
444
|
+
)
|
|
445
|
+
if include_branch:
|
|
446
|
+
checks.extend(
|
|
447
|
+
_validate_branch(
|
|
448
|
+
None,
|
|
449
|
+
config=normalized_config,
|
|
450
|
+
repo_path=normalized_repo_path,
|
|
451
|
+
config_path=normalized_config_path,
|
|
452
|
+
)["checks"]
|
|
453
|
+
)
|
|
454
|
+
if include_author:
|
|
455
|
+
checks.extend(
|
|
456
|
+
_validate_author(
|
|
457
|
+
None,
|
|
458
|
+
None,
|
|
459
|
+
config=normalized_config,
|
|
460
|
+
repo_path=normalized_repo_path,
|
|
461
|
+
config_path=normalized_config_path,
|
|
462
|
+
)["checks"]
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
"status": "fail" if any(c["status"] == "fail" for c in checks) else "pass",
|
|
467
|
+
"checks": checks,
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@mcp.tool()
|
|
472
|
+
def describe_validation_rules(
|
|
473
|
+
config: dict[str, Any] | None = None,
|
|
474
|
+
repo_path: str | None = None,
|
|
475
|
+
config_path: str | None = None,
|
|
476
|
+
) -> dict[str, Any]:
|
|
477
|
+
"""Return enabled commit-check rules after merging defaults, repo config, and overrides."""
|
|
478
|
+
normalized_repo_path = _normalize_repo_path(repo_path)
|
|
479
|
+
normalized_config = _normalize_config(config)
|
|
480
|
+
normalized_config_path = _normalize_config_path(config_path, normalized_repo_path)
|
|
481
|
+
merged_config = _merge_config(
|
|
482
|
+
normalized_config,
|
|
483
|
+
repo_path=normalized_repo_path,
|
|
484
|
+
config_path=normalized_config_path,
|
|
485
|
+
)
|
|
486
|
+
rules = [rule.to_dict() for rule in RuleBuilder(merged_config).build_all_rules()]
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
"commit_check_version": commit_check_version,
|
|
490
|
+
"config": merged_config,
|
|
491
|
+
"supported_checks": list(
|
|
492
|
+
dict.fromkeys(entry.check for entry in COMMIT_RULES + BRANCH_RULES)
|
|
493
|
+
),
|
|
494
|
+
"enabled_rules": rules,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def main() -> None:
|
|
499
|
+
"""Run commit-check MCP server via stdio transport."""
|
|
500
|
+
mcp.run(transport="stdio")
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
if __name__ == "__main__": # pragma: no cover
|
|
504
|
+
main()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: commit-check-mcp
|
|
3
|
+
Version: 0.1.1.post1.dev1
|
|
4
|
+
Summary: MCP server exposing commit-check validation tools
|
|
5
|
+
Author: commit-check
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: commit-check<3,>=2.5.0
|
|
11
|
+
Requires-Dist: mcp<2,>=1.27.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest<10,>=9.0.0; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# commit-check-mcp
|
|
17
|
+
|
|
18
|
+
Model Context Protocol (MCP) server for [commit-check](https://github.com/commit-check/commit-check).
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
This MCP server exposes commit-check validations as MCP tools:
|
|
23
|
+
|
|
24
|
+
- `server_health` — returns server/sdk versions
|
|
25
|
+
- `validate_commit_message` — validates a commit message
|
|
26
|
+
- `validate_branch_name` — validates a branch name or the current repo branch
|
|
27
|
+
- `validate_author_info` — validates author name/email or the repo's git author config
|
|
28
|
+
- `validate_commit_context` — runs combined checks in one call
|
|
29
|
+
- `validate_repository_state` — validates latest commit, current branch, and author state for a repo
|
|
30
|
+
- `describe_validation_rules` — returns the effective config and enabled rules after merging defaults and repo config
|
|
31
|
+
|
|
32
|
+
All validation tools return the same structured commit-check result shape:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"status": "pass|fail",
|
|
37
|
+
"checks": [
|
|
38
|
+
{
|
|
39
|
+
"check": "message",
|
|
40
|
+
"status": "pass|fail",
|
|
41
|
+
"value": "...",
|
|
42
|
+
"error": "...",
|
|
43
|
+
"suggest": "..."
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Run
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
commit-check-mcp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The server runs over stdio transport (recommended MCP default for local tool integrations).
|
|
62
|
+
|
|
63
|
+
## Repository-Aware Validation
|
|
64
|
+
|
|
65
|
+
`commit-check` is most useful when it runs against a real git repository and its `cchk.toml` or `commit-check.toml` file. This MCP server now supports that directly:
|
|
66
|
+
|
|
67
|
+
- `repo_path` — run git-based validations against a specific repository
|
|
68
|
+
- `config_path` — point to an explicit TOML config file; relative paths are resolved from `repo_path`
|
|
69
|
+
- `config` — apply ad-hoc overrides on top of defaults and repo config
|
|
70
|
+
|
|
71
|
+
Typical patterns:
|
|
72
|
+
|
|
73
|
+
- Validate an explicit message with a repository's rules
|
|
74
|
+
- Validate the current repository state without passing message/branch/author values manually
|
|
75
|
+
- Inspect which rules are actually enabled after config merging
|
|
76
|
+
|
|
77
|
+
Example payload for a repository-wide validation:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"repo_path": "/path/to/repo",
|
|
82
|
+
"include_message": true,
|
|
83
|
+
"include_branch": true,
|
|
84
|
+
"include_author": true
|
|
85
|
+
}
|
|
86
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
commit_check_mcp/__init__.py,sha256=0qlKOLxQwzq4RmQlxbdAO2UT7y0zKqS2RQq-PQs7jkM,141
|
|
2
|
+
commit_check_mcp/server.py,sha256=aZfgzLQp_pWKYgMm49IZRBCOuQiMiscMU6ltfr5OJIo,17505
|
|
3
|
+
commit_check_mcp-0.1.1.post1.dev1.dist-info/licenses/LICENSE,sha256=XMRJzGhsjuBcVso4JnpcvzaDBl7N1TR4WU_Y6E8f6Wk,1069
|
|
4
|
+
commit_check_mcp-0.1.1.post1.dev1.dist-info/METADATA,sha256=W_qoI2-LwxjVUACHNOZTqOVaGjxD0mgzFiMQG_OvuaQ,2483
|
|
5
|
+
commit_check_mcp-0.1.1.post1.dev1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
commit_check_mcp-0.1.1.post1.dev1.dist-info/entry_points.txt,sha256=Aj84jmw43ZbHhkLA2W_tFQSMj0cn_xw8GAlL-44trH4,66
|
|
7
|
+
commit_check_mcp-0.1.1.post1.dev1.dist-info/top_level.txt,sha256=VPx0wL4_vLMrJSZou2krNUhaa8NAjlqbgJKjN2co4ic,17
|
|
8
|
+
commit_check_mcp-0.1.1.post1.dev1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Commit Check
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
commit_check_mcp
|