commit-message-ai-mcp 1.0.0__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.
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: commit-message-ai-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AI-powered commit message ai MCP server for agents. Supports generate commit, parse diff, suggest type. By MEOK AI Labs.
|
|
5
|
+
Project-URL: Homepage, https://meok.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/CSOAI-ORG/commit-message-ai-mcp
|
|
7
|
+
Author-email: MEOK AI Labs <nicholas@meok.ai>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 MEOK AI Labs
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Keywords: ai,commit,mcp,meok,message
|
|
23
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
24
|
+
Classifier: Operating System :: OS Independent
|
|
25
|
+
Classifier: Programming Language :: Python :: 3
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: mcp>=1.0.0
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
server.py,sha256=9GDcd5Cv-0Rhv8DLCmvwyfpWXyN52EMdAdIr34KzD6w,11651
|
|
2
|
+
commit_message_ai_mcp-1.0.0.dist-info/METADATA,sha256=aDRlKo70S4BhoP2hoVhVG8jDeZIPGk6Ub_UZp26zywA,1378
|
|
3
|
+
commit_message_ai_mcp-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
4
|
+
commit_message_ai_mcp-1.0.0.dist-info/entry_points.txt,sha256=VDySpf-Euddcr8WlJ4nU7cVfTMMsWw-tF4pmS8SPUqg,54
|
|
5
|
+
commit_message_ai_mcp-1.0.0.dist-info/licenses/LICENSE,sha256=ibFbFVuWMg3hkFJtLijRTUi6DDoUbdR4oE78M6MKq-I,607
|
|
6
|
+
commit_message_ai_mcp-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MEOK AI Labs
|
|
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.
|
server.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate conventional commit messages from diffs and descriptions. — MEOK AI Labs."""
|
|
3
|
+
|
|
4
|
+
import sys, os
|
|
5
|
+
sys.path.insert(0, os.path.expanduser('~/clawd/meok-labs-engine/shared'))
|
|
6
|
+
from auth_middleware import check_access
|
|
7
|
+
|
|
8
|
+
import json, re
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from mcp.server.fastmcp import FastMCP
|
|
12
|
+
|
|
13
|
+
FREE_DAILY_LIMIT = 30
|
|
14
|
+
_usage = defaultdict(list)
|
|
15
|
+
def _rl(c="anon"):
|
|
16
|
+
now = datetime.now(timezone.utc)
|
|
17
|
+
_usage[c] = [t for t in _usage[c] if (now-t).total_seconds() < 86400]
|
|
18
|
+
if len(_usage[c]) >= FREE_DAILY_LIMIT: return json.dumps({"error": "Limit {0}/day. Upgrade: meok.ai".format(FREE_DAILY_LIMIT)})
|
|
19
|
+
_usage[c].append(now); return None
|
|
20
|
+
|
|
21
|
+
mcp = FastMCP("commit-message-ai", instructions="MEOK AI Labs — Generate conventional commit messages from diffs and descriptions.")
|
|
22
|
+
|
|
23
|
+
COMMIT_TYPES = {
|
|
24
|
+
"feat": {"description": "A new feature", "semver": "minor", "changelog": "Features"},
|
|
25
|
+
"fix": {"description": "A bug fix", "semver": "patch", "changelog": "Bug Fixes"},
|
|
26
|
+
"docs": {"description": "Documentation only changes", "semver": None, "changelog": "Documentation"},
|
|
27
|
+
"style": {"description": "Code style changes (formatting, semicolons)", "semver": None, "changelog": "Styles"},
|
|
28
|
+
"refactor": {"description": "Code refactoring without feature/fix", "semver": None, "changelog": "Refactoring"},
|
|
29
|
+
"perf": {"description": "Performance improvements", "semver": "patch", "changelog": "Performance"},
|
|
30
|
+
"test": {"description": "Adding or fixing tests", "semver": None, "changelog": "Tests"},
|
|
31
|
+
"build": {"description": "Build system or dependencies", "semver": None, "changelog": "Build"},
|
|
32
|
+
"ci": {"description": "CI configuration changes", "semver": None, "changelog": "CI"},
|
|
33
|
+
"chore": {"description": "Other changes (no src/test)", "semver": None, "changelog": "Chores"},
|
|
34
|
+
"revert": {"description": "Reverts a previous commit", "semver": "patch", "changelog": "Reverts"},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
TYPE_KEYWORDS = {
|
|
38
|
+
"fix": ["fix", "bug", "error", "crash", "issue", "patch", "resolve", "correct", "repair"],
|
|
39
|
+
"feat": ["add", "new", "feature", "implement", "create", "introduce", "support"],
|
|
40
|
+
"refactor": ["refactor", "restructure", "clean", "improve", "simplify", "reorganize", "extract"],
|
|
41
|
+
"docs": ["doc", "readme", "comment", "documentation", "guide", "wiki", "changelog"],
|
|
42
|
+
"test": ["test", "spec", "coverage", "assert", "mock", "fixture"],
|
|
43
|
+
"perf": ["perf", "performance", "optimize", "speed", "fast", "cache", "lazy"],
|
|
44
|
+
"style": ["style", "format", "lint", "whitespace", "indent", "semicolon"],
|
|
45
|
+
"ci": ["ci", "pipeline", "workflow", "deploy", "github actions", "jenkins"],
|
|
46
|
+
"build": ["build", "dependency", "package", "webpack", "vite", "rollup", "npm", "pip"],
|
|
47
|
+
"chore": ["chore", "misc", "update", "bump", "release"],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
SCOPES_BY_FILE_PATTERN = {
|
|
51
|
+
r"auth|login|session|password": "auth",
|
|
52
|
+
r"api|endpoint|route|controller": "api",
|
|
53
|
+
r"ui|component|view|page|template": "ui",
|
|
54
|
+
r"db|migration|model|schema|query": "db",
|
|
55
|
+
r"test|spec|fixture": "test",
|
|
56
|
+
r"config|setting|env": "config",
|
|
57
|
+
r"docker|k8s|deploy|ci": "infra",
|
|
58
|
+
r"doc|readme|changelog": "docs",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _detect_type(text: str) -> str:
|
|
63
|
+
text_lower = text.lower()
|
|
64
|
+
scores = defaultdict(int)
|
|
65
|
+
for commit_type, keywords in TYPE_KEYWORDS.items():
|
|
66
|
+
for kw in keywords:
|
|
67
|
+
if kw in text_lower:
|
|
68
|
+
scores[commit_type] += 1
|
|
69
|
+
if scores:
|
|
70
|
+
return max(scores, key=scores.get)
|
|
71
|
+
return "chore"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _detect_scope(text: str) -> str:
|
|
75
|
+
text_lower = text.lower()
|
|
76
|
+
for pattern, scope in SCOPES_BY_FILE_PATTERN.items():
|
|
77
|
+
if re.search(pattern, text_lower):
|
|
78
|
+
return scope
|
|
79
|
+
return ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _is_breaking(text: str) -> bool:
|
|
83
|
+
indicators = ["breaking", "BREAKING CHANGE", "incompatible", "remove api",
|
|
84
|
+
"drop support", "migration required", "not backward"]
|
|
85
|
+
return any(ind.lower() in text.lower() for ind in indicators)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@mcp.tool()
|
|
89
|
+
def generate_commit_message(changes_description: str, commit_type: str = "auto",
|
|
90
|
+
scope: str = "", breaking: bool = False, api_key: str = "") -> str:
|
|
91
|
+
"""Generate a conventional commit message from a description. Auto-detects type, scope, and breaking changes."""
|
|
92
|
+
allowed, msg, tier = check_access(api_key)
|
|
93
|
+
if not allowed:
|
|
94
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
95
|
+
if err := _rl(): return err
|
|
96
|
+
|
|
97
|
+
detected_type = commit_type if commit_type != "auto" else _detect_type(changes_description)
|
|
98
|
+
detected_scope = scope or _detect_scope(changes_description)
|
|
99
|
+
is_breaking = breaking or _is_breaking(changes_description)
|
|
100
|
+
|
|
101
|
+
subject = changes_description.strip().split("\n")[0][:72]
|
|
102
|
+
if subject[0:1].isupper():
|
|
103
|
+
subject = subject[0].lower() + subject[1:]
|
|
104
|
+
subject = subject.rstrip(".")
|
|
105
|
+
|
|
106
|
+
scope_part = f"({detected_scope})" if detected_scope else ""
|
|
107
|
+
bang = "!" if is_breaking else ""
|
|
108
|
+
message = f"{detected_type}{scope_part}{bang}: {subject}"
|
|
109
|
+
|
|
110
|
+
body_lines = changes_description.strip().split("\n")[1:]
|
|
111
|
+
body = "\n".join(line.strip() for line in body_lines if line.strip()) if body_lines else ""
|
|
112
|
+
footer = "BREAKING CHANGE: " + subject if is_breaking else ""
|
|
113
|
+
|
|
114
|
+
full_message = message
|
|
115
|
+
if body:
|
|
116
|
+
full_message += f"\n\n{body}"
|
|
117
|
+
if footer:
|
|
118
|
+
full_message += f"\n\n{footer}"
|
|
119
|
+
|
|
120
|
+
type_info = COMMIT_TYPES.get(detected_type, COMMIT_TYPES["chore"])
|
|
121
|
+
return {
|
|
122
|
+
"message": full_message,
|
|
123
|
+
"subject_line": message,
|
|
124
|
+
"type": detected_type,
|
|
125
|
+
"scope": detected_scope,
|
|
126
|
+
"breaking": is_breaking,
|
|
127
|
+
"semver_impact": "major" if is_breaking else type_info["semver"],
|
|
128
|
+
"changelog_section": type_info["changelog"],
|
|
129
|
+
"char_count": len(message),
|
|
130
|
+
"valid": len(message) <= 72,
|
|
131
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@mcp.tool()
|
|
136
|
+
def analyze_diff(diff_text: str, api_key: str = "") -> str:
|
|
137
|
+
"""Parse a git diff and produce a structured summary with files changed, additions, deletions, and suggested commit type."""
|
|
138
|
+
allowed, msg, tier = check_access(api_key)
|
|
139
|
+
if not allowed:
|
|
140
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
141
|
+
if err := _rl(): return err
|
|
142
|
+
|
|
143
|
+
lines = diff_text.split("\n")
|
|
144
|
+
files_changed = []
|
|
145
|
+
additions = 0
|
|
146
|
+
deletions = 0
|
|
147
|
+
current_file = None
|
|
148
|
+
|
|
149
|
+
for line in lines:
|
|
150
|
+
if line.startswith("diff --git"):
|
|
151
|
+
parts = line.split(" b/")
|
|
152
|
+
if len(parts) > 1:
|
|
153
|
+
current_file = parts[1].strip()
|
|
154
|
+
files_changed.append(current_file)
|
|
155
|
+
elif line.startswith("+") and not line.startswith("+++"):
|
|
156
|
+
additions += 1
|
|
157
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
158
|
+
deletions += 1
|
|
159
|
+
|
|
160
|
+
all_files_text = " ".join(files_changed)
|
|
161
|
+
suggested_type = _detect_type(all_files_text + " " + diff_text[:500])
|
|
162
|
+
suggested_scope = _detect_scope(all_files_text)
|
|
163
|
+
|
|
164
|
+
file_types = defaultdict(int)
|
|
165
|
+
for f in files_changed:
|
|
166
|
+
ext = os.path.splitext(f)[1] if "." in f else "no_ext"
|
|
167
|
+
file_types[ext] += 1
|
|
168
|
+
|
|
169
|
+
size = "small" if additions + deletions < 20 else "medium" if additions + deletions < 100 else "large"
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"files_changed": files_changed,
|
|
173
|
+
"total_files": len(files_changed),
|
|
174
|
+
"additions": additions,
|
|
175
|
+
"deletions": deletions,
|
|
176
|
+
"net_change": additions - deletions,
|
|
177
|
+
"change_size": size,
|
|
178
|
+
"file_types": dict(file_types),
|
|
179
|
+
"suggested_type": suggested_type,
|
|
180
|
+
"suggested_scope": suggested_scope,
|
|
181
|
+
"is_breaking": _is_breaking(diff_text),
|
|
182
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@mcp.tool()
|
|
187
|
+
def suggest_type(description: str, api_key: str = "") -> str:
|
|
188
|
+
"""Suggest the best conventional commit type for a change description with confidence scoring."""
|
|
189
|
+
allowed, msg, tier = check_access(api_key)
|
|
190
|
+
if not allowed:
|
|
191
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
192
|
+
if err := _rl(): return err
|
|
193
|
+
|
|
194
|
+
text_lower = description.lower()
|
|
195
|
+
scores = {}
|
|
196
|
+
for commit_type, keywords in TYPE_KEYWORDS.items():
|
|
197
|
+
score = sum(1 for kw in keywords if kw in text_lower)
|
|
198
|
+
if score > 0:
|
|
199
|
+
scores[commit_type] = score
|
|
200
|
+
|
|
201
|
+
if not scores:
|
|
202
|
+
return {"suggested_type": "chore", "confidence": 0.3, "all_scores": {},
|
|
203
|
+
"available_types": list(COMMIT_TYPES.keys()),
|
|
204
|
+
"description": COMMIT_TYPES["chore"]["description"],
|
|
205
|
+
"timestamp": datetime.now(timezone.utc).isoformat()}
|
|
206
|
+
|
|
207
|
+
total = sum(scores.values())
|
|
208
|
+
ranked = sorted(scores.items(), key=lambda x: -x[1])
|
|
209
|
+
best_type = ranked[0][0]
|
|
210
|
+
confidence = round(ranked[0][1] / max(total, 1), 2)
|
|
211
|
+
|
|
212
|
+
alternatives = [{"type": t, "score": s, "description": COMMIT_TYPES[t]["description"]}
|
|
213
|
+
for t, s in ranked[1:4]]
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
"suggested_type": best_type,
|
|
217
|
+
"confidence": min(confidence, 1.0),
|
|
218
|
+
"description": COMMIT_TYPES[best_type]["description"],
|
|
219
|
+
"semver_impact": COMMIT_TYPES[best_type]["semver"],
|
|
220
|
+
"alternatives": alternatives,
|
|
221
|
+
"breaking_detected": _is_breaking(description),
|
|
222
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@mcp.tool()
|
|
227
|
+
def validate_conventional(message: str, api_key: str = "") -> str:
|
|
228
|
+
"""Validate a commit message against the Conventional Commits specification and report issues."""
|
|
229
|
+
allowed, msg, tier = check_access(api_key)
|
|
230
|
+
if not allowed:
|
|
231
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
232
|
+
if err := _rl(): return err
|
|
233
|
+
|
|
234
|
+
errors = []
|
|
235
|
+
warnings = []
|
|
236
|
+
lines = message.strip().split("\n")
|
|
237
|
+
subject = lines[0] if lines else ""
|
|
238
|
+
|
|
239
|
+
pattern = r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-zA-Z0-9_-]+\))?(!)?: .+"
|
|
240
|
+
match = re.match(pattern, subject)
|
|
241
|
+
if not match:
|
|
242
|
+
errors.append("Subject line does not match conventional commit format: type(scope): description")
|
|
243
|
+
else:
|
|
244
|
+
parsed_type = match.group(1)
|
|
245
|
+
if parsed_type not in COMMIT_TYPES:
|
|
246
|
+
errors.append(f"Unknown type '{parsed_type}'. Valid: {', '.join(COMMIT_TYPES.keys())}")
|
|
247
|
+
|
|
248
|
+
if len(subject) > 72:
|
|
249
|
+
warnings.append(f"Subject line is {len(subject)} chars (recommended max 72)")
|
|
250
|
+
if len(subject) > 100:
|
|
251
|
+
errors.append(f"Subject line is {len(subject)} chars (hard limit 100)")
|
|
252
|
+
|
|
253
|
+
if subject and subject[-1] == ".":
|
|
254
|
+
warnings.append("Subject line should not end with a period")
|
|
255
|
+
if subject and subject.split(": ", 1)[-1][0:1].isupper():
|
|
256
|
+
warnings.append("Description after type should start with lowercase")
|
|
257
|
+
|
|
258
|
+
if len(lines) > 1 and lines[1].strip() != "":
|
|
259
|
+
warnings.append("Second line should be blank (separating subject from body)")
|
|
260
|
+
|
|
261
|
+
has_breaking_footer = any(line.startswith("BREAKING CHANGE:") for line in lines)
|
|
262
|
+
has_bang = "!" in subject.split(":")[0] if ":" in subject else False
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
"valid": len(errors) == 0,
|
|
266
|
+
"subject": subject,
|
|
267
|
+
"errors": errors,
|
|
268
|
+
"warnings": warnings,
|
|
269
|
+
"parsed": {
|
|
270
|
+
"type": match.group(1) if match else None,
|
|
271
|
+
"scope": (match.group(2) or "").strip("()") if match else None,
|
|
272
|
+
"breaking": has_bang or has_breaking_footer,
|
|
273
|
+
"description": subject.split(": ", 1)[-1] if ": " in subject else subject,
|
|
274
|
+
},
|
|
275
|
+
"char_count": len(subject),
|
|
276
|
+
"body_present": len(lines) > 2,
|
|
277
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
mcp.run()
|