mcp-udacity-commit
Advanced tools
+32
-2
@@ -5,4 +5,4 @@ #!/usr/bin/env node | ||
| import { z } from "zod"; | ||
| import { STYLE_GUIDE, validate, formatMessage } from "./lint.js"; | ||
| const VERSION = "1.0.2"; | ||
| import { STYLE_GUIDE, BRANCH_GUIDE, validate, formatMessage, validateBranch } from "./lint.js"; | ||
| const VERSION = "1.2.0"; | ||
| export function createServer() { | ||
@@ -15,2 +15,7 @@ const server = new McpServer({ name: "udacity-commit", version: VERSION }); | ||
| }, async (uri) => ({ contents: [{ uri: uri.href, text: STYLE_GUIDE }] })); | ||
| server.registerResource("branch-naming", "udacity://branch-naming", { | ||
| title: "Branch Naming (companion convention)", | ||
| description: "type/kebab-case branch-naming rules that pair with the commit style.", | ||
| mimeType: "text/markdown", | ||
| }, async (uri) => ({ contents: [{ uri: uri.href, text: BRANCH_GUIDE }] })); | ||
| server.registerTool("validate_commit_message", { | ||
@@ -74,2 +79,27 @@ title: "Validate a commit message", | ||
| }); | ||
| server.registerTool("validate_branch_name", { | ||
| title: "Validate a git branch name", | ||
| description: "Check a git branch name against the companion type/kebab-case convention " + | ||
| '(e.g. "feat/add-dark-mode"). Returns whether it is compliant plus any problems ' + | ||
| "(violations) and warnings (hints). Base branches like main/master are exempt.", | ||
| inputSchema: { name: z.string().describe('The branch name to check, e.g. "feat/add-dark-mode"') }, | ||
| outputSchema: { | ||
| valid: z.boolean(), | ||
| problems: z.array(z.string()), | ||
| warnings: z.array(z.string()), | ||
| }, | ||
| }, async ({ name }) => { | ||
| const r = validateBranch(name); | ||
| const text = [ | ||
| r.valid ? "✅ Compliant branch name." : "❌ Not compliant.", | ||
| ...r.problems.map((p) => ` • ${p}`), | ||
| ...r.warnings.map((w) => ` ⚠ ${w}`), | ||
| ].join("\n"); | ||
| const structuredContent = { | ||
| valid: r.valid, | ||
| problems: r.problems, | ||
| warnings: r.warnings, | ||
| }; | ||
| return { content: [{ type: "text", text }], structuredContent }; | ||
| }); | ||
| return server; | ||
@@ -76,0 +106,0 @@ } |
+107
-0
@@ -13,2 +13,9 @@ /** | ||
| export const BODY_WRAP = 72; | ||
| export const BRANCH_MAX = 50; | ||
| /** | ||
| * Long-lived / protected branches that are exempt from the feature-branch | ||
| * `type/description` rule. `release/*` is NOT here — it is validated as a | ||
| * typed branch with a version-style description (e.g. `release/1.2.0`). | ||
| */ | ||
| export const BASE_BRANCHES = new Set(["main", "master", "dev", "develop", "trunk"]); | ||
| export const TYPES = { | ||
@@ -23,2 +30,8 @@ feat: "A new feature", | ||
| }; | ||
| /** | ||
| * Types allowed as a branch prefix: the commit types plus `release`, which is | ||
| * a branch-only type (release branches are named, not committed). `release` | ||
| * takes a version-style description; every other type takes kebab-case. | ||
| */ | ||
| export const BRANCH_TYPES = [...Object.keys(TYPES), "release"]; | ||
| /** Footer keywords recognized for issue-reference validation. */ | ||
@@ -91,2 +104,28 @@ export const FOOTER_KEYS = ["Resolves", "Closes", "Fixes", "Fix", "See also", "Refs", "Ref"]; | ||
| `; | ||
| export const BRANCH_GUIDE = `# Branch naming (companion convention) | ||
| Not part of the official Udacity *commit-message* guide, but a natural | ||
| companion: name feature branches after the change they carry, reusing the | ||
| same commit \`type\` set. | ||
| type/kebab-case-description | ||
| ## Rules | ||
| - \`type/\` prefix — one of: ${BRANCH_TYPES.join(", ")} | ||
| - A single \`/\` separates the type from the description | ||
| - Description is **kebab-case**: lowercase letters and digits joined by single | ||
| hyphens (\`feat/add-dark-mode\`, not \`feat/Add_Dark_Mode\`) | ||
| - \`release/\` branches take a **version-style** description instead: lowercase | ||
| words/digits joined by dots or hyphens (\`release/1.2.0\`, \`release/2024-q1\`) | ||
| - No spaces, underscores, uppercase, or leading/trailing/double hyphens | ||
| - Keep it short — ${BRANCH_MAX} characters or fewer (a hint, not a hard limit) | ||
| ## Examples | ||
| - \`feat/add-dark-mode\` | ||
| - \`fix/duplicate-auth-refresh\` | ||
| - \`chore/bump-deps\` | ||
| - \`release/1.2.0\` | ||
| Base branches (${[...BASE_BRANCHES].join(", ")}) are exempt. | ||
| `; | ||
| /** Length in Unicode code points (not UTF-16 code units). */ | ||
@@ -215,1 +254,69 @@ export function width(s) { | ||
| } | ||
| /** Kebab-case: lowercase words (letters/digits) joined by single hyphens. */ | ||
| const KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/; | ||
| /** | ||
| * Version-style (for `release/` branches): lowercase words/digits joined by | ||
| * dots or hyphens, so `1.2.0`, `2.0.0-rc1`, and `2024-q1` all pass. | ||
| */ | ||
| const RELEASE_DESC = /^[a-z0-9]+([.-][a-z0-9]+)*$/; | ||
| /** | ||
| * Validate a git branch name against the companion `type/kebab-case` | ||
| * convention. Base/long-lived branches (main, master, dev, develop, trunk) | ||
| * are accepted as-is with a note. `release/` is a typed branch and takes a | ||
| * version-style description (e.g. `release/1.2.0`). | ||
| */ | ||
| export function validateBranch(name) { | ||
| const problems = []; | ||
| const warnings = []; | ||
| const branch = name.trim(); | ||
| if (!branch) { | ||
| return { valid: false, problems: ["Branch name is empty."], warnings }; | ||
| } | ||
| if (branch !== name) { | ||
| problems.push("Branch name has leading/trailing whitespace."); | ||
| } | ||
| // Base / long-lived branches are exempt from the feature-branch rule. | ||
| // (`release/*` is NOT exempt — it is validated as a typed branch below.) | ||
| if (BASE_BRANCHES.has(branch)) { | ||
| warnings.push(`"${branch}" is a base branch — feature-branch naming rules don't apply.`); | ||
| return { valid: problems.length === 0, problems, warnings }; | ||
| } | ||
| const slash = branch.indexOf("/"); | ||
| if (slash === -1) { | ||
| problems.push(`Branch must follow "type/description". Got: "${branch}".`); | ||
| return { valid: false, problems, warnings }; | ||
| } | ||
| const type = branch.slice(0, slash); | ||
| const description = branch.slice(slash + 1); | ||
| if (!BRANCH_TYPES.includes(type)) { | ||
| problems.push(`Unknown type "${type}". Use one of: ${BRANCH_TYPES.join(", ")}.`); | ||
| } | ||
| // `release/` takes a version-style description; every other type is kebab-case. | ||
| const isRelease = type === "release"; | ||
| const pattern = isRelease ? RELEASE_DESC : KEBAB; | ||
| if (!description) { | ||
| problems.push("Description after the type is empty."); | ||
| } | ||
| else if (description.includes("/")) { | ||
| problems.push(`Use a single "/" after the type; the description must not contain "/". Got: "${description}".`); | ||
| } | ||
| else if (!pattern.test(description)) { | ||
| // Give the most specific reason we can, else a general shape message. | ||
| if (/[A-Z]/.test(description)) { | ||
| problems.push(`Description must be lowercase ${isRelease ? "version-style (e.g. 1.2.0)" : "kebab-case"}. Got: "${description}".`); | ||
| } | ||
| else if (/[_ ]/.test(description)) { | ||
| problems.push("Use hyphens, not spaces or underscores, to separate words."); | ||
| } | ||
| else if (isRelease) { | ||
| problems.push(`Release description must be version-style: lowercase words/digits joined by dots or hyphens (e.g. "1.2.0", "2024-q1"). Got: "${description}".`); | ||
| } | ||
| else { | ||
| problems.push(`Description must be kebab-case: lowercase words joined by single hyphens. Got: "${description}".`); | ||
| } | ||
| } | ||
| if (width(branch) > BRANCH_MAX) { | ||
| warnings.push(`Branch name is ${width(branch)} chars; keep it ${BRANCH_MAX} or fewer.`); | ||
| } | ||
| return { valid: problems.length === 0, problems, warnings }; | ||
| } |
+1
-1
| { | ||
| "name": "mcp-udacity-commit", | ||
| "version": "1.0.2", | ||
| "version": "1.2.0", | ||
| "description": "MCP server that validates and formats git commit messages per the Udacity Git Commit Message Style Guide.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+13
-0
@@ -51,4 +51,6 @@ # mcp-udacity-commit | ||
| | Resource | `udacity://commit-styleguide` | The style-guide rules, as markdown | | ||
| | Resource | `udacity://branch-naming` | The companion `type/kebab-case` branch-naming rules, as markdown | | ||
| | Tool | `validate_commit_message` | Checks a message against every rule (type, ≤50-char subject, capitalization, no trailing period, blank line, ≤72-char body wrap) | | ||
| | Tool | `format_commit_message` | Builds a compliant message from `type` + `subject` + optional `body`/`footer` | | ||
| | Tool | `validate_branch_name` | Checks a branch name against the companion `type/kebab-case` convention (e.g. `feat/add-dark-mode`); `release/*` is a typed branch with a version-style description (`release/1.2.0`), and base branches like `main` are exempt | | ||
@@ -82,2 +84,13 @@ ## Example | ||
| `validate_branch_name` enforces the companion `type/kebab-case` convention: | ||
| ```text | ||
| "feat/add-dark-mode" → ✅ Compliant branch name. | ||
| "release/1.2.0" → ✅ Compliant branch name. | ||
| "Feature/Add_Dark_Mode" → ❌ Not compliant. | ||
| • Unknown type "Feature". Use one of: feat, fix, docs, style, refactor, test, chore, release. | ||
| • Description must be lowercase kebab-case. Got: "Add_Dark_Mode". | ||
| "main" → ✅ (base branch — feature-branch rules don't apply) | ||
| ``` | ||
| ## Develop | ||
@@ -84,0 +97,0 @@ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
24192
43.22%415
46.64%105
14.13%0
-100%