Skip to content

Commit f8d5108

Browse files
authored
feat: describe every tool parameter and annotate tools for MCP clients (#35)
* feat: describe every tool parameter and annotate tools for MCP clients An MCP client hands the model the tool's JSON input schema, not its docstring. Until now every property carried only pydantic's auto title (push_refs: string | null), so the pre-push line format, the "omit to check upstream" behaviour and the "non-empty when provided" rule were discovered by trial and error. Every parameter now has a description through Annotated[..., Field(description=...)], which the SDK preserves in the schema; the shared ones (config, repo_path, config_path, branch, author_*) are defined once as module-level aliases. Every tool also gets a display title and ToolAnnotations with readOnlyHint=True, destructiveHint=False and idempotentHint=True, so hosts that gate calls on those hints can auto-approve them. openWorldHint is True only for validate_push_safety and validate_repository_state, because the force-push rule may run `git fetch <remote> <ref>` to resolve a SHA it does not know locally. MCPServer now receives version=__version__, so serverInfo.version is no longer an empty string. The tool descriptions are rewritten around one shared paragraph that defines the result: {status, warnings, checks[]}, that only 'fail' is a rejection and 'skip' means nothing was validated, what 'warn' and 'skip' mean per check, and that rule_id and docs_url exist. Each tool keeps its own "when to use" sentence. validate_push_safety no longer claims that push.allow_force_push can re-enable force pushes: _validate_push forces that flag off after merging config, so the tool always rejects them. The server instructions now describe the loop an agent should follow: validate first, read status, apply a non-empty fix verbatim or follow suggest, validate again, and consult describe_validation_rules before guessing at a format. Tests list the tools through the in-process server API and assert the titles, annotations, open-world split, per-property descriptions, the version and the instructions. README documents rule_id/docs_url, the schema metadata, and that config_path replaces the repository's own config file rather than merging with it. * fix: reject unresolvable push SHAs and stop calling fetching tools read-only Two review findings on the previous commit. The force-push rule answers with `git merge-base --is-ancestor`. When a SHA in push_refs is not a commit the repository knows, git exits 128, commit-check tries to fetch the remote ref, and if the SHA is still unknown the rule falls through to PASS. validate_push_safety therefore reported `status: pass` for made-up SHAs, and an agent would read that as clearance to push. After the rule has run (so a SHA its fetch brought in counts as resolved), _validate_push now checks every SHA in explicit push_refs with `git cat-file -e <sha>^{commit}` inside repo_path, skipping the 40-zero new-branch placeholder, and raises a ToolError naming the SHA when one is not a commit. An engine fail still wins: a rejection is already the right answer. The upstream-fallback path (push_refs omitted, and validate_repository_state's include_push) reads HEAD and the upstream ref, which always resolve, and is unchanged. validate_push_safety and validate_repository_state may run `git fetch`, which writes FETCH_HEAD and remote-tracking refs, so advertising them with readOnlyHint=True was not honest. _tool now takes `fetches` and sets readOnlyHint=False, openWorldHint=True for those two; the other six keep readOnlyHint=True, openWorldHint=False. destructiveHint stays False and idempotentHint True everywhere: repeating the same fetch has no further effect. The tool descriptions, push_refs description, server instructions and README say the same thing instead of "read-only". Tests: fake SHAs are a tool error, a real fast-forward pair passes, a real history rewrite fails, a zero remote SHA passes, an engine fail is not turned into an error by a later unresolvable line, repository_state include_push is unaffected, and the annotation split is asserted.
1 parent d20bc2c commit f8d5108

3 files changed

Lines changed: 557 additions & 128 deletions

File tree

README.md

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ This MCP server exposes commit-check validations as MCP tools:
1919
- `server_health` — returns server/sdk versions
2020
- `validate_commit_message` — validates a commit message
2121
- `validate_branch_name` — validates a branch name or the current repo branch
22-
- `validate_push_safety` — validates that a push is not a force push
22+
- `validate_push_safety` — validates that a push is not a force push (force pushes are always rejected by this tool)
2323
- `validate_author_info` — validates author name/email or the repo's git author config
2424
- `validate_commit_context` — runs combined checks in one call
2525
- `validate_repository_state` — validates latest commit, current branch, author state, and optional push safety for a repo
@@ -33,17 +33,22 @@ All validation tools return the same structured commit-check result shape:
3333
"warnings": 0,
3434
"checks": [
3535
{
36+
"rule_id": "CC001",
3637
"check": "message",
3738
"status": "pass|fail|warn|skip",
3839
"value": "...",
3940
"error": "...",
4041
"suggest": "...",
41-
"fix": "..."
42+
"fix": "...",
43+
"docs_url": "https://commit-check.com/rules/#cc001"
4244
}
4345
]
4446
}
4547
```
4648

49+
`rule_id` is the stable id of the rule that produced the check and `docs_url`
50+
links to its documentation.
51+
4752
Only `fail` is a rejection. A check reports `skip` when it did not run — the
4853
author matched `ignore_authors`, or there was nothing to check — and the
4954
top-level `status` is `skip` only when **every** check skipped, so a run that
@@ -59,9 +64,14 @@ a non-empty `fix` as it stands and fall back to `suggest` when it is empty.
5964
A call that cannot run at all — an empty `message`, a `repo_path` that does not
6065
exist, a `repo_path` that is not a git repository when the tool has to read git
6166
state (see the `repo_path` note under [Tool Usage](#tool-usage)), a malformed or rejected
62-
commit-check config — is returned as an MCP tool error (`is_error`) whose text
63-
names the problem, for example `repo_path is not a git repository: /path/to/dir`
64-
or `invalid commit-check config: ...`, rather than as a `pass`/`fail` result.
67+
commit-check config, a `push_refs` SHA that is not a commit in `repo_path` even
68+
after the force-push check tried to fetch it — is returned as an MCP tool error
69+
(`is_error`) whose text names the problem, for example
70+
`repo_path is not a git repository: /path/to/dir`,
71+
`invalid commit-check config: ...` or
72+
`push_refs: <sha> is not a commit in the repository; fetch it first, the force-push check cannot be judged`,
73+
rather than as a `pass`/`fail` result. In particular a push whose SHAs cannot
74+
be judged is never reported as a pass.
6575

6676
## Installation
6777

@@ -266,10 +276,26 @@ After the client starts the server, it will expose these tools:
266276
- `validate_repository_state(repo_path?, config?, config_path?, include_message?, include_branch?, include_author?, include_push?)`
267277
- `describe_validation_rules(config?, repo_path?, config_path?)`
268278

279+
Every parameter carries a description in the tool's JSON input schema, so an
280+
MCP client (and the model behind it) can see what each one expects without
281+
reading this file: for example `push_refs` documents the git pre-push line
282+
format `<local_ref> <local_sha> <remote_ref> <remote_sha>`. Each tool also has
283+
a display `title` and MCP tool annotations: `destructiveHint: false` and
284+
`idempotentHint: true` everywhere, `readOnlyHint: true` on the six tools that
285+
only read, and `readOnlyHint: false` with `openWorldHint: true` on
286+
`validate_push_safety` and `validate_repository_state`, because the force-push
287+
check may run `git fetch` to resolve a SHA, which updates `FETCH_HEAD` and
288+
remote-tracking refs (the working tree and commits are never touched). Clients
289+
that gate tool calls on those hints can auto-approve the read-only six. The
290+
server's `instructions` describe the intended
291+
loop: validate first, read `status` (only `fail` rejects, `skip` is not
292+
approval), apply a non-empty `fix` verbatim or follow `suggest`, then validate
293+
again.
294+
269295
The common optional arguments are:
270296

271-
- `repo_path`: repository directory to validate against; it must be a git repository when the tool reads git state (branch, author, or push refs omitted, or `validate_repository_state`), and may be a plain directory holding a config file when every value is supplied
272-
- `config_path`: explicit TOML config file; relative paths resolve from `repo_path`
297+
- `repo_path`: repository directory to validate against; it must be a git repository when the tool reads git state (branch, author, or push refs omitted, `validate_repository_state`, or `push_refs` given, whose SHAs must resolve there), and may be a plain directory holding a config file when every other value is supplied
298+
- `config_path`: explicit TOML config file, used instead of the repository's own `cchk.toml`/`commit-check.toml`; relative paths resolve from `repo_path`
273299
- `config`: ad-hoc config overrides merged on top of defaults and repo config
274300

275301
## Common Examples
@@ -355,9 +381,8 @@ Example payload for a repository-wide validation:
355381
Config precedence is:
356382

357383
1. `commit-check` built-in defaults
358-
2. repository config loaded from `repo_path`
359-
3. `config_path` when explicitly provided
360-
4. inline `config` overrides passed to the tool
384+
2. repository config loaded from `repo_path`, or the file named by `config_path` when it is provided (it replaces the repository's own config file)
385+
3. inline `config` overrides passed to the tool
361386

362387
## Published On
363388

0 commit comments

Comments
 (0)