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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ commit_message_ai_mcp = server:main
@@ -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()