mcp-udacity-commit 1.0.0 → 1.0.2

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muzaffar Qosimov
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.
package/README.md CHANGED
@@ -1,18 +1,17 @@
1
1
  # mcp-udacity-commit
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/mcp-udacity-commit)](https://www.npmjs.com/package/mcp-udacity-commit)
4
+ [![npm downloads](https://img.shields.io/npm/dm/mcp-udacity-commit)](https://www.npmjs.com/package/mcp-udacity-commit)
5
+ [![License: MIT](https://img.shields.io/npm/l/mcp-udacity-commit)](./LICENSE)
6
+ [![MCP](https://img.shields.io/badge/MCP-server-blue)](https://modelcontextprotocol.io)
7
+
3
8
  An [MCP](https://modelcontextprotocol.io) server that validates and formats git
4
9
  commit messages according to the
5
10
  [Udacity Git Commit Message Style Guide](https://udacity.github.io/git-styleguide/).
6
11
 
7
- ## What it exposes
8
-
9
- | Primitive | Name | Purpose |
10
- | --- | --- | --- |
11
- | Resource | `udacity://commit-styleguide` | The style-guide rules, as markdown |
12
- | Tool | `validate_commit_message` | Checks a message against every rule (type, ≤50-char subject, capitalization, no trailing period, blank line, ≤72-char body wrap) |
13
- | Tool | `format_commit_message` | Builds a compliant message from `type` + `subject` + optional `body`/`footer` |
12
+ ## Install
14
13
 
15
- ## Install (published package)
14
+ One line paste it into your terminal:
16
15
 
17
16
  ```bash
18
17
  claude mcp add udacity-commit -- npx -y mcp-udacity-commit
@@ -31,7 +30,8 @@ For Claude Desktop, add to `claude_desktop_config.json`:
31
30
  }
32
31
  ```
33
32
 
34
- ## Install from source
33
+ <details>
34
+ <summary>Install from source</summary>
35
35
 
36
36
  ```bash
37
37
  git clone https://github.com/qwertymuzaffar/mcp-udacity-commit
@@ -41,6 +41,43 @@ npm run build
41
41
  claude mcp add udacity-commit -- node "$(pwd)/build/index.js"
42
42
  ```
43
43
 
44
+ </details>
45
+
46
+ ## What it exposes
47
+
48
+ | Primitive | Name | Purpose |
49
+ | --- | --- | --- |
50
+ | Resource | `udacity://commit-styleguide` | The style-guide rules, as markdown |
51
+ | Tool | `validate_commit_message` | Checks a message against every rule (type, ≤50-char subject, capitalization, no trailing period, blank line, ≤72-char body wrap) |
52
+ | Tool | `format_commit_message` | Builds a compliant message from `type` + `subject` + optional `body`/`footer` |
53
+
54
+ ## Example
55
+
56
+ `format_commit_message` turns loose parts into a compliant commit:
57
+
58
+ ```text
59
+ in: type=fix subject="prevent duplicate auth token refresh."
60
+ body="The refresh timer could fire twice under load, minting two tokens…"
61
+ footer="Resolves: #142"
62
+
63
+ out:
64
+ fix: Prevent duplicate auth token refresh
65
+
66
+ The refresh timer could fire twice under load, minting two tokens and
67
+ logging the user out. Serialize refreshes behind a single in-flight
68
+ promise so concurrent callers await the same request.
69
+
70
+ Resolves: #142
71
+ ```
72
+
73
+ `validate_commit_message` flags every violation:
74
+
75
+ ```text
76
+ "Fixed the login bug." → ❌ Not compliant.
77
+ • Subject must follow "type: Subject".
78
+ • Subject must not end with a period.
79
+ ```
80
+
44
81
  ## Develop
45
82
 
46
83
  ```bash
@@ -49,17 +86,6 @@ npm run build # → build/index.js
49
86
  npm run test:client # spawns the server and exercises the tools
50
87
  ```
51
88
 
52
- ## Publish
53
-
54
- ```bash
55
- # 1. npm
56
- npm publish --access public
57
-
58
- # 2. MCP Registry (after npm publish)
59
- mcp-publisher login github
60
- mcp-publisher publish
61
- ```
62
-
63
89
  ## License
64
90
 
65
91
  MIT
package/build/index.js CHANGED
@@ -2,158 +2,80 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- const SUBJECT_MAX = 50;
6
- const BODY_WRAP = 72;
7
- const TYPES = {
8
- feat: "A new feature",
9
- fix: "A bug fix",
10
- docs: "Changes to documentation",
11
- style: "Formatting, missing semicolons, etc; no code change",
12
- refactor: "Refactoring production code",
13
- test: "Adding tests, refactoring tests; no production code change",
14
- chore: "Updating build tasks, package configs, etc; no production code change",
15
- };
16
- const STYLE_GUIDE = `# Udacity Git Commit Message Style Guide
17
-
18
- A commit message has three parts separated by blank lines: **subject**, optional **body**, optional **footer**.
19
-
20
- type: Subject
21
-
22
- Body — the what and why, not the how.
23
-
24
- Resolves: #123
25
- See also: #456, #789
26
-
27
- ## Types
28
- ${Object.entries(TYPES)
29
- .map(([t, d]) => `- **${t}**: ${d}`)
30
- .join("\n")}
31
-
32
- ## Subject
33
- - \`type: Subject\` format
34
- - No more than ${SUBJECT_MAX} characters
35
- - Begins with a capital letter
36
- - Imperative mood ("Add", not "Added")
37
- - No trailing period
38
- - Blank line separates it from the body
39
-
40
- ## Body (optional)
41
- - Only when the commit needs explanation
42
- - Explains the **what** and **why**, not the how
43
- - Wrap each line at ${BODY_WRAP} characters
44
-
45
- ## Footer (optional)
46
- - References issue-tracker IDs: \`Resolves: #123\`, \`See also: #456, #789\`
47
- `;
48
- /** Greedy word-wrap that preserves existing paragraph breaks. */
49
- function wrap(text, width) {
50
- return text
51
- .split("\n")
52
- .map((para) => {
53
- const words = para.split(/\s+/).filter(Boolean);
54
- const lines = [];
55
- let line = "";
56
- for (const w of words) {
57
- if (!line)
58
- line = w;
59
- else if ((line + " " + w).length <= width)
60
- line += " " + w;
61
- else {
62
- lines.push(line);
63
- line = w;
64
- }
65
- }
66
- if (line)
67
- lines.push(line);
68
- return lines.join("\n");
69
- })
70
- .join("\n");
5
+ import { STYLE_GUIDE, validate, formatMessage } from "./lint.js";
6
+ const VERSION = "1.0.2";
7
+ export function createServer() {
8
+ const server = new McpServer({ name: "udacity-commit", version: VERSION });
9
+ server.registerResource("styleguide", "udacity://commit-styleguide", {
10
+ title: "Udacity Git Commit Style Guide",
11
+ description: "The commit-message rules (types, subject, body, footer).",
12
+ mimeType: "text/markdown",
13
+ }, async (uri) => ({ contents: [{ uri: uri.href, text: STYLE_GUIDE }] }));
14
+ server.registerTool("validate_commit_message", {
15
+ title: "Validate a commit message",
16
+ description: "Check a commit message against the Udacity Git Commit Message Style Guide. " +
17
+ "Returns whether it is compliant plus any problems (violations) and warnings (hints).",
18
+ inputSchema: { message: z.string().describe("The full commit message to check") },
19
+ outputSchema: {
20
+ valid: z.boolean(),
21
+ problems: z.array(z.string()),
22
+ warnings: z.array(z.string()),
23
+ },
24
+ }, async ({ message }) => {
25
+ const r = validate(message);
26
+ const text = [
27
+ r.valid ? "✅ Compliant with the Udacity style guide." : "❌ Not compliant.",
28
+ ...r.problems.map((p) => ` • ${p}`),
29
+ ...r.warnings.map((w) => ` ⚠ ${w}`),
30
+ ].join("\n");
31
+ const structuredContent = {
32
+ valid: r.valid,
33
+ problems: r.problems,
34
+ warnings: r.warnings,
35
+ };
36
+ return { content: [{ type: "text", text }], structuredContent };
37
+ });
38
+ server.registerTool("format_commit_message", {
39
+ title: "Format a Udacity-style commit message",
40
+ description: "Compose a compliant commit message from its parts. The subject is capitalized, " +
41
+ "a trailing period is removed, and the body is wrapped at 72 characters.",
42
+ inputSchema: {
43
+ type: z.enum(["feat", "fix", "docs", "style", "refactor", "test", "chore"]),
44
+ subject: z.string().describe("Imperative subject; auto-capitalized, trailing period removed"),
45
+ body: z.string().optional().describe("What & why; auto-wrapped at 72 chars"),
46
+ footer: z.string().optional().describe('Issue refs, e.g. "Resolves: #123"'),
47
+ },
48
+ outputSchema: {
49
+ message: z.string(),
50
+ valid: z.boolean(),
51
+ problems: z.array(z.string()),
52
+ warnings: z.array(z.string()),
53
+ },
54
+ }, async (input) => {
55
+ const { message, report } = formatMessage(input);
56
+ const note = !report.valid
57
+ ? "❌ " + [...report.problems, ...report.warnings].join("; ")
58
+ : report.warnings.length
59
+ ? "✅ compliant (hint: " + report.warnings.join("; ") + ")"
60
+ : " compliant";
61
+ const structuredContent = {
62
+ message,
63
+ valid: report.valid,
64
+ problems: report.problems,
65
+ warnings: report.warnings,
66
+ };
67
+ return {
68
+ content: [{ type: "text", text: `${message}\n\n--- ${note}` }],
69
+ structuredContent,
70
+ };
71
+ });
72
+ return server;
71
73
  }
72
- function validate(message) {
73
- const problems = [];
74
- const warnings = [];
75
- const lines = message.replace(/\s+$/, "").split("\n");
76
- const subject = lines[0] ?? "";
77
- const m = subject.match(/^(\w+): (.*)$/);
78
- if (!m) {
79
- problems.push(`Subject must follow "type: Subject". Got: "${subject}".`);
80
- }
81
- else {
82
- const [, type, rest] = m;
83
- if (!TYPES[type]) {
84
- problems.push(`Unknown type "${type}". Use one of: ${Object.keys(TYPES).join(", ")}.`);
85
- }
86
- if (!rest) {
87
- problems.push("Subject text is empty after the type.");
88
- }
89
- else {
90
- if (!/^[A-Z]/.test(rest)) {
91
- problems.push(`Subject should begin with a capital letter (got "${rest[0]}").`);
92
- }
93
- const first = rest.split(/\s+/)[0];
94
- if (/(ed|ing)$/i.test(first)) {
95
- warnings.push(`"${first}" looks past-tense/gerund — use imperative mood ("Add", not "Added").`);
96
- }
97
- }
98
- }
99
- if (/\.$/.test(subject))
100
- problems.push("Subject must not end with a period.");
101
- if (subject.length > SUBJECT_MAX) {
102
- problems.push(`Subject is ${subject.length} chars; max is ${SUBJECT_MAX}.`);
103
- }
104
- if (lines.length > 1 && lines[1].trim() !== "") {
105
- problems.push("Leave a blank line between the subject and the body.");
106
- }
107
- for (let i = 2; i < lines.length; i++) {
108
- if (lines[i].length > BODY_WRAP) {
109
- problems.push(`Line ${i + 1} is ${lines[i].length} chars; wrap body/footer at ${BODY_WRAP}.`);
110
- }
111
- }
112
- return { valid: problems.length === 0, problems, warnings };
74
+ async function main() {
75
+ const server = createServer();
76
+ await server.connect(new StdioServerTransport());
113
77
  }
114
- const server = new McpServer({ name: "udacity-commit", version: "1.0.0" });
115
- server.registerResource("styleguide", "udacity://commit-styleguide", {
116
- title: "Udacity Git Commit Style Guide",
117
- description: "The commit-message rules (types, subject, body, footer).",
118
- mimeType: "text/markdown",
119
- }, async (uri) => ({ contents: [{ uri: uri.href, text: STYLE_GUIDE }] }));
120
- server.registerTool("validate_commit_message", {
121
- title: "Validate a commit message",
122
- description: "Check a commit message against the Udacity Git Commit Message Style Guide.",
123
- inputSchema: { message: z.string().describe("The full commit message to check") },
124
- }, async ({ message }) => {
125
- const r = validate(message);
126
- const out = [
127
- r.valid ? "✅ Compliant with the Udacity style guide." : "❌ Not compliant.",
128
- ...r.problems.map((p) => ` • ${p}`),
129
- ...r.warnings.map((w) => ` ⚠ ${w}`),
130
- ].join("\n");
131
- return { content: [{ type: "text", text: out }] };
78
+ main().catch((err) => {
79
+ console.error(err);
80
+ process.exit(1);
132
81
  });
133
- server.registerTool("format_commit_message", {
134
- title: "Format a Udacity-style commit message",
135
- description: "Compose a compliant commit message from its parts.",
136
- inputSchema: {
137
- type: z.enum(["feat", "fix", "docs", "style", "refactor", "test", "chore"]),
138
- subject: z
139
- .string()
140
- .describe("Imperative subject; auto-capitalized, trailing period removed"),
141
- body: z.string().optional().describe("What & why; auto-wrapped at 72 chars"),
142
- footer: z.string().optional().describe('Issue refs, e.g. "Resolves: #123"'),
143
- },
144
- }, async ({ type, subject, body, footer }) => {
145
- let s = subject.trim().replace(/\.+$/, "");
146
- s = s.charAt(0).toUpperCase() + s.slice(1);
147
- const parts = [`${type}: ${s}`];
148
- if (body?.trim())
149
- parts.push("", wrap(body.trim(), BODY_WRAP));
150
- if (footer?.trim())
151
- parts.push("", footer.trim());
152
- const msg = parts.join("\n");
153
- const v = validate(msg);
154
- const note = v.valid ? "✅ compliant" : "❌ " + v.problems.join("; ");
155
- return { content: [{ type: "text", text: `${msg}\n\n--- ${note}` }] };
156
- });
157
- const transport = new StdioServerTransport();
158
- await server.connect(transport);
159
- //# sourceMappingURL=index.js.map
package/build/lint.js ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Pure, side-effect-free linting logic for the Udacity Git Commit Message
3
+ * Style Guide. Kept separate from the MCP server (index.ts) so it can be
4
+ * unit-tested in isolation.
5
+ *
6
+ * Length semantics: all limits are measured in Unicode code points
7
+ * (`width()`), not UTF-16 code units, so emoji / CJK / combining marks are
8
+ * counted the way a human reads them. The 50-char subject limit applies to
9
+ * the WHOLE `type: Subject` line, including the `type: ` prefix.
10
+ */
11
+ export const SUBJECT_MAX = 50;
12
+ export const BODY_WRAP = 72;
13
+ export const TYPES = {
14
+ feat: "A new feature",
15
+ fix: "A bug fix",
16
+ docs: "Changes to documentation",
17
+ style: "Formatting, missing semicolons, etc; no code change",
18
+ refactor: "Refactoring production code",
19
+ test: "Adding tests, refactoring tests; no production code change",
20
+ chore: "Updating build tasks, package configs, etc; no production code change",
21
+ };
22
+ /** Footer keywords recognized for issue-reference validation. */
23
+ export const FOOTER_KEYS = ["Resolves", "Closes", "Fixes", "Fix", "See also", "Refs", "Ref"];
24
+ /**
25
+ * Common non-imperative first words (past tense / gerund). We match against
26
+ * this allow-known-bad list rather than a broad `/(ed|ing)$/` regex so that
27
+ * legitimate imperatives like "Bring", "Embed", "Ring" are never flagged.
28
+ * This favors precision (no false positives) over recall.
29
+ */
30
+ const NON_IMPERATIVE = new Set([
31
+ // past tense
32
+ "added", "fixed", "updated", "changed", "removed", "deleted", "created",
33
+ "refactored", "implemented", "improved", "renamed", "moved", "merged",
34
+ "reverted", "bumped", "cleaned", "corrected", "adjusted", "enabled",
35
+ "disabled", "introduced", "resolved", "replaced", "converted", "migrated",
36
+ "dropped", "extracted", "wrapped", "tweaked", "optimized", "simplified",
37
+ "formatted", "documented", "tested", "released", "handled", "allowed",
38
+ "prevented", "ensured", "avoided", "unified", "applied", "upgraded",
39
+ "downgraded", "patched", "hardened", "restructured", "reorganized",
40
+ "deprecated", "exposed", "integrated", "validated", "normalized", "cached",
41
+ "supported", "added", "wired", "hooked",
42
+ // gerund
43
+ "adding", "fixing", "updating", "changing", "removing", "deleting",
44
+ "creating", "refactoring", "implementing", "improving", "renaming",
45
+ "moving", "merging", "reverting", "bumping", "cleaning", "correcting",
46
+ "adjusting", "enabling", "disabling", "introducing", "resolving",
47
+ "replacing", "converting", "migrating", "dropping", "extracting",
48
+ "wrapping", "tweaking", "optimizing", "simplifying", "formatting",
49
+ "documenting", "testing", "releasing", "handling", "allowing",
50
+ "preventing", "ensuring", "avoiding", "unifying", "applying", "upgrading",
51
+ "patching", "hardening", "restructuring", "reorganizing", "deprecating",
52
+ "exposing", "integrating", "validating", "normalizing", "caching",
53
+ "supporting",
54
+ ]);
55
+ export const STYLE_GUIDE = `# Udacity Git Commit Message Style Guide
56
+
57
+ A commit message has three parts separated by blank lines: **subject**, optional **body**, optional **footer**.
58
+
59
+ type: Subject
60
+
61
+ Body — the what and why, not the how.
62
+
63
+ Resolves: #123
64
+ See also: #456, #789
65
+
66
+ ## Types
67
+ ${Object.entries(TYPES)
68
+ .map(([t, d]) => `- **${t}**: ${d}`)
69
+ .join("\n")}
70
+
71
+ ## Subject
72
+ - \`type: Subject\` format
73
+ - The whole line (including the \`type: \` prefix) is no more than ${SUBJECT_MAX} characters
74
+ - Begins with a capital letter
75
+ - Imperative mood ("Add", not "Added")
76
+ - No trailing period
77
+ - Blank line separates it from the body
78
+
79
+ ## Body (optional)
80
+ - Only when the commit needs explanation
81
+ - Explains the **what** and **why**, not the how
82
+ - Wrap each line at ${BODY_WRAP} characters
83
+
84
+ ## Footer (optional)
85
+ - References issue-tracker IDs: \`Resolves: #123\`, \`See also: #456, #789\`
86
+
87
+ _Lengths are counted in Unicode code points._
88
+ `;
89
+ /** Length in Unicode code points (not UTF-16 code units). */
90
+ export function width(s) {
91
+ return [...s].length;
92
+ }
93
+ /** Uppercase the first code point of a string (astral-safe). */
94
+ function capitalizeFirst(s) {
95
+ const chars = [...s];
96
+ if (chars.length === 0)
97
+ return s;
98
+ return chars[0].toUpperCase() + chars.slice(1).join("");
99
+ }
100
+ /**
101
+ * Greedy word-wrap that preserves paragraph breaks AND hard-breaks any single
102
+ * token longer than `max` (URLs, long paths), so no output line ever exceeds
103
+ * the limit. This guarantees `format`'s output always passes `validate`.
104
+ */
105
+ export function wrap(text, max = BODY_WRAP) {
106
+ return text
107
+ .split("\n")
108
+ .map((para) => {
109
+ const lines = [];
110
+ let line = "";
111
+ const flush = () => {
112
+ if (line) {
113
+ lines.push(line);
114
+ line = "";
115
+ }
116
+ };
117
+ for (let word of para.split(/\s+/).filter(Boolean)) {
118
+ // hard-break an over-long token across multiple lines
119
+ while (width(word) > max) {
120
+ flush();
121
+ const chars = [...word];
122
+ lines.push(chars.slice(0, max).join(""));
123
+ word = chars.slice(max).join("");
124
+ }
125
+ if (!line)
126
+ line = word;
127
+ else if (width(line) + 1 + width(word) <= max)
128
+ line += " " + word;
129
+ else {
130
+ flush();
131
+ line = word;
132
+ }
133
+ }
134
+ flush();
135
+ return lines.join("\n");
136
+ })
137
+ .join("\n");
138
+ }
139
+ /** Validate a full commit message against the Udacity style guide. */
140
+ export function validate(message) {
141
+ const problems = [];
142
+ const warnings = [];
143
+ // Normalize CRLF / lone CR so line-based checks are reliable.
144
+ const normalized = message.replace(/\r\n?/g, "\n");
145
+ const lines = normalized.split("\n");
146
+ // Drop trailing blank lines (a trailing newline shouldn't count as a body).
147
+ while (lines.length > 1 && lines[lines.length - 1].trim() === "")
148
+ lines.pop();
149
+ const rawSubject = lines[0] ?? "";
150
+ const subject = rawSubject.replace(/\s+$/, "");
151
+ const m = subject.match(/^(\w+): (.*)$/);
152
+ if (!m) {
153
+ problems.push(`Subject must follow "type: Subject". Got: "${subject}".`);
154
+ }
155
+ else {
156
+ const [, type, rest] = m;
157
+ if (!TYPES[type]) {
158
+ problems.push(`Unknown type "${type}". Use one of: ${Object.keys(TYPES).join(", ")}.`);
159
+ }
160
+ if (!rest) {
161
+ problems.push("Subject text is empty after the type.");
162
+ }
163
+ else {
164
+ if (/^\p{Ll}/u.test(rest)) {
165
+ problems.push(`Subject should begin with a capital letter (got "${[...rest][0]}").`);
166
+ }
167
+ const firstWord = rest.split(/\s+/)[0];
168
+ if (NON_IMPERATIVE.has(firstWord.toLowerCase())) {
169
+ warnings.push(`"${firstWord}" looks past-tense/gerund — use the imperative mood ("Add", not "Added").`);
170
+ }
171
+ }
172
+ }
173
+ if (rawSubject !== subject)
174
+ problems.push("Subject has trailing whitespace.");
175
+ if (/\.$/.test(subject))
176
+ problems.push("Subject must not end with a period.");
177
+ if (width(subject) > SUBJECT_MAX) {
178
+ problems.push(`Subject line is ${width(subject)} chars (incl. the "type: " prefix); max is ${SUBJECT_MAX}.`);
179
+ }
180
+ if (lines.length > 1 && lines[1].trim() !== "") {
181
+ problems.push("Leave a blank line between the subject and the body.");
182
+ }
183
+ for (let i = 2; i < lines.length; i++) {
184
+ const line = lines[i];
185
+ if (width(line) > BODY_WRAP) {
186
+ problems.push(`Line ${i + 1} is ${width(line)} chars; wrap body/footer at ${BODY_WRAP}.`);
187
+ }
188
+ const fm = line.match(/^([A-Za-z][A-Za-z ]*?):\s*(.*)$/);
189
+ if (fm && FOOTER_KEYS.some((k) => k.toLowerCase() === fm[1].toLowerCase())) {
190
+ if (!/#\d+/.test(fm[2])) {
191
+ warnings.push(`Footer "${fm[1]}" should reference an issue, e.g. "${fm[1]}: #123".`);
192
+ }
193
+ }
194
+ }
195
+ return { valid: problems.length === 0, problems, warnings };
196
+ }
197
+ /**
198
+ * Compose a compliant commit message from parts. Capitalizes the subject,
199
+ * strips a trailing period, and hard-wraps the body so the result always
200
+ * passes `validate`.
201
+ */
202
+ export function formatMessage(input) {
203
+ const subject = capitalizeFirst(input.subject.trim().replace(/\.+$/, ""));
204
+ const parts = [`${input.type}: ${subject}`];
205
+ if (input.body?.trim())
206
+ parts.push("", wrap(input.body.trim(), BODY_WRAP));
207
+ if (input.footer?.trim())
208
+ parts.push("", input.footer.trim());
209
+ const message = parts.join("\n");
210
+ return { message, report: validate(message) };
211
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-udacity-commit",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "MCP server that validates and formats git commit messages per the Udacity Git Commit Message Style Guide.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,9 +29,11 @@
29
29
  "node": ">=18"
30
30
  },
31
31
  "scripts": {
32
- "build": "tsc && chmod 755 build/index.js",
32
+ "build": "rm -rf build && tsc && chmod 755 build/index.js",
33
33
  "start": "node build/index.js",
34
- "prepublishOnly": "npm run build",
34
+ "pretest": "npm run build",
35
+ "test": "node --test test/*.test.mjs",
36
+ "prepublishOnly": "npm test",
35
37
  "test:client": "node test-client.mjs"
36
38
  },
37
39
  "dependencies": {
package/build/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}