豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 1088931

Browse files
feat: improve UX with neutral messaging and fail_on_diff option
- Change '⚠️ API Differences Detected' to '📋 API Changes Detected' for less alarming tone - Remove <details> nesting for diffs - show them directly for easier reading - Add fail_on_diff option to fail the action when API changes are detected (for release validation) - Show ref names (branch/tag) instead of just SHA in reports where possible - Add links to language starter repos and github-mcp-server in README - Update README overview to focus on visibility rather than alarming about breaking changes
1 parent 0590d2f commit 1088931

File tree

10 files changed

+232
-50
lines changed

10 files changed

+232
-50
lines changed

README.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
[![GitHub release](https://img.shields.io/github/v/release/SamMorrowDrums/mcp-conformance-action)](https://github.com/SamMorrowDrums/mcp-conformance-action/releases)
55
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
66

7-
A GitHub Action for detecting changes to [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server **public interfaces**. This action compares the current branch against a baseline to surface any changes to your server's exposed tools, resources, prompts, and capabilities—helping you catch unintended breaking changes and document intentional API evolution.
7+
A GitHub Action for detecting changes to [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server **public interfaces**. This action compares the current branch against a baseline to surface any changes to your server's exposed tools, resources, prompts, and capabilities—helping you document API evolution and catch unintended modifications.
88

99
## Overview
1010

11-
MCP servers expose a **public interface** to AI assistants: tools (with their input schemas), resources, prompts, and server capabilities. As your server evolves, changes to this interface can break clients or alter expected behavior. This action automates public interface comparison by:
11+
MCP servers expose a **public interface** to AI assistants: tools (with their input schemas), resources, prompts, and server capabilities. As your server evolves, changes to this interface are worth tracking. This action automates public interface comparison by:
1212

1313
1. Building your MCP server from both the current branch and a baseline (merge-base, tag, or specified ref)
1414
2. Querying both versions for their complete public interface (tools, resources, prompts, capabilities)
15-
3. Generating a detailed diff report showing exactly what changed
15+
3. Generating a diff report showing exactly what changed
1616
4. Surfacing results directly in GitHub's Job Summary
1717

1818
This is **not** about testing internal logic or correctness—it's about visibility into what your server _advertises_ to clients.
@@ -200,6 +200,8 @@ Either `start_command` (for stdio) or `server_url` (for HTTP) must be provided,
200200
| Input | Description | Default |
201201
|-------|-------------|---------|
202202
| `compare_ref` | Git ref to compare against. Auto-detects merge-base on PRs or previous tag on tag pushes if not specified. | `""` |
203+
| `fail_on_diff` | Fail the action if API changes are detected. Useful for release validation workflows. | `false` |
204+
| `fail_on_error` | Fail the action if probe errors occur (connection failures, etc.) | `true` |
203205

204206
### Configuration Object Schema
205207

@@ -354,6 +356,21 @@ Specify any git ref to compare against:
354356
compare_ref: v1.0.0
355357
```
356358

359+
### Failing on Changes (Release Validation)
360+
361+
For release workflows where you want to ensure no API changes, use `fail_on_diff`:
362+
363+
```yaml
364+
- uses: SamMorrowDrums/mcp-conformance-action@v2
365+
with:
366+
setup_node: true
367+
install_command: npm ci
368+
build_command: npm run build
369+
start_command: node dist/stdio.js
370+
compare_ref: v1.0.0
371+
fail_on_diff: true # Action fails if any API changes are detected
372+
```
373+
357374
## Artifacts and Reports
358375

359376
The action produces:
@@ -439,3 +456,17 @@ Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for gu
439456
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
440457
- [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)
441458
- [MCP Go SDK](https://github.com/modelcontextprotocol/go-sdk)
459+
460+
### Example Configurations
461+
462+
Working examples of this action in various languages:
463+
464+
| Language | Repository | Workflow |
465+
|----------|------------|----------|
466+
| TypeScript | [mcp-typescript-starter](https://github.com/SamMorrowDrums/mcp-typescript-starter) | [conformance.yml](https://github.com/SamMorrowDrums/mcp-typescript-starter/blob/main/.github/workflows/conformance.yml) |
467+
| Python | [mcp-python-starter](https://github.com/SamMorrowDrums/mcp-python-starter) | [conformance.yml](https://github.com/SamMorrowDrums/mcp-python-starter/blob/main/.github/workflows/conformance.yml) |
468+
| Go | [mcp-go-starter](https://github.com/SamMorrowDrums/mcp-go-starter) | [conformance.yml](https://github.com/SamMorrowDrums/mcp-go-starter/blob/main/.github/workflows/conformance.yml) |
469+
| Rust | [mcp-rust-starter](https://github.com/SamMorrowDrums/mcp-rust-starter) | [conformance.yml](https://github.com/SamMorrowDrums/mcp-rust-starter/blob/main/.github/workflows/conformance.yml) |
470+
| C# | [mcp-csharp-starter](https://github.com/SamMorrowDrums/mcp-csharp-starter) | [conformance.yml](https://github.com/SamMorrowDrums/mcp-csharp-starter/blob/main/.github/workflows/conformance.yml) |
471+
472+
For a production example, see [github-mcp-server](https://github.com/github/github-mcp-server).

action.yml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ inputs:
104104
description: 'Fail the action if probe errors occur (not just API differences)'
105105
required: false
106106
default: 'true'
107+
fail_on_diff:
108+
description: 'Fail the action if API differences are detected (useful for release validation)'
109+
required: false
110+
default: 'false'
107111
env_vars:
108112
description: 'Environment variables (newline-separated KEY=VALUE pairs)'
109113
required: false
@@ -232,15 +236,9 @@ runs:
232236
echo "" >> $GITHUB_STEP_SUMMARY
233237
234238
if [ "${{ steps.conformance.outputs.status }}" = "passed" ]; then
235-
echo "**All conformance tests passed** - No behavioral differences detected." >> $GITHUB_STEP_SUMMARY
239+
echo "**No API changes detected** between the branches." >> $GITHUB_STEP_SUMMARY
236240
else
237-
echo "**Differences detected** - Review the diffs above to ensure changes are intentional." >> $GITHUB_STEP_SUMMARY
238-
echo "" >> $GITHUB_STEP_SUMMARY
239-
echo "Common expected differences:" >> $GITHUB_STEP_SUMMARY
240-
echo "- New tools added" >> $GITHUB_STEP_SUMMARY
241-
echo "- Tool descriptions updated" >> $GITHUB_STEP_SUMMARY
242-
echo "- New resources or prompts" >> $GITHUB_STEP_SUMMARY
243-
echo "- Capability changes" >> $GITHUB_STEP_SUMMARY
241+
echo "**API changes detected** — review above to confirm they are expected." >> $GITHUB_STEP_SUMMARY
244242
fi
245243
246244
- name: Upload conformance report

dist/git.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,8 @@ export declare function checkout(ref: string): Promise<void>;
3030
* Checkout previous branch/ref
3131
*/
3232
export declare function checkoutPrevious(): Promise<void>;
33+
/**
34+
* Get a display-friendly name for a ref.
35+
* Returns branch/tag name if available, otherwise the short SHA.
36+
*/
37+
export declare function getRefDisplayName(ref: string): Promise<string>;

dist/index.js

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35472,6 +35472,76 @@ async function checkoutPrevious() {
3547235472
// Ignore errors
3547335473
}
3547435474
}
35475+
/**
35476+
* Get a display-friendly name for a ref.
35477+
* Returns branch/tag name if available, otherwise the short SHA.
35478+
*/
35479+
async function getRefDisplayName(ref) {
35480+
// If it's already a readable name (not a SHA), return it
35481+
if (!ref.match(/^[a-f0-9]{40}$/i) && !ref.match(/^[a-f0-9]{7,}$/i)) {
35482+
// It's likely already a branch/tag name
35483+
return ref;
35484+
}
35485+
// Try to find a branch name pointing to this ref
35486+
let output = "";
35487+
try {
35488+
await exec.exec("git", ["branch", "--points-at", ref, "--format=%(refname:short)"], {
35489+
silent: true,
35490+
listeners: {
35491+
stdout: (data) => {
35492+
output += data.toString();
35493+
},
35494+
},
35495+
});
35496+
const branches = output.trim().split("\n").filter(Boolean);
35497+
if (branches.length > 0) {
35498+
// Prefer main/master if available
35499+
if (branches.includes("main"))
35500+
return "main";
35501+
if (branches.includes("master"))
35502+
return "master";
35503+
return branches[0];
35504+
}
35505+
}
35506+
catch {
35507+
// Ignore errors
35508+
}
35509+
// Try to find a tag pointing to this ref
35510+
output = "";
35511+
try {
35512+
await exec.exec("git", ["tag", "--points-at", ref], {
35513+
silent: true,
35514+
listeners: {
35515+
stdout: (data) => {
35516+
output += data.toString();
35517+
},
35518+
},
35519+
});
35520+
const tags = output.trim().split("\n").filter(Boolean);
35521+
if (tags.length > 0) {
35522+
return tags[0];
35523+
}
35524+
}
35525+
catch {
35526+
// Ignore errors
35527+
}
35528+
// Fall back to short SHA
35529+
output = "";
35530+
try {
35531+
await exec.exec("git", ["rev-parse", "--short", ref], {
35532+
silent: true,
35533+
listeners: {
35534+
stdout: (data) => {
35535+
output += data.toString();
35536+
},
35537+
},
35538+
});
35539+
return output.trim() || ref;
35540+
}
35541+
catch {
35542+
return ref.substring(0, 7);
35543+
}
35544+
}
3547535545

3547635546
// EXTERNAL MODULE: external "path"
3547735547
var external_path_ = __nccwpck_require__(6928);
@@ -53804,10 +53874,10 @@ async function runAllTests(ctx) {
5380453874
result.diffs = compareResults(branchFiles, baseFiles);
5380553875
result.hasDifferences = result.diffs.size > 0;
5380653876
if (result.hasDifferences) {
53807-
lib_core.warning(`⚠️ Configuration ${config.name}: ${result.diffs.size} differences found`);
53877+
lib_core.info(`📋 Configuration ${config.name}: ${result.diffs.size} change(s) found`);
5380853878
}
5380953879
else {
53810-
lib_core.info(`✅ Configuration ${config.name}: no differences`);
53880+
lib_core.info(`✅ Configuration ${config.name}: no changes`);
5381153881
}
5381253882
// Save individual result
5381353883
const resultPath = external_path_.join(ctx.workDir, ".conformance-results", `${config.name}.json`);
@@ -53871,14 +53941,14 @@ function generateMarkdownReport(report) {
5387153941
lines.push("");
5387253942
// Overall status
5387353943
if (report.diffCount === 0) {
53874-
lines.push("## ✅ All Conformance Tests Passed");
53944+
lines.push("## ✅ No API Changes");
5387553945
lines.push("");
53876-
lines.push("No API differences detected between the current branch and the comparison ref.");
53946+
lines.push("No differences detected between the current branch and the comparison ref.");
5387753947
}
5387853948
else {
53879-
lines.push("## ⚠️ API Differences Detected");
53949+
lines.push("## 📋 API Changes Detected");
5388053950
lines.push("");
53881-
lines.push(`${report.diffCount} configuration(s) have API differences that may indicate breaking changes.`);
53951+
lines.push(`${report.diffCount} configuration(s) have changes. Review below to ensure they are intentional.`);
5388253952
}
5388353953
lines.push("");
5388453954
// Per-configuration results
@@ -53893,18 +53963,15 @@ function generateMarkdownReport(report) {
5389353963
lines.push(`- **Base Time:** ${formatTime(result.baseTime)}`);
5389453964
lines.push("");
5389553965
if (result.hasDifferences) {
53896-
lines.push("#### Differences");
53966+
lines.push("#### Changes");
5389753967
lines.push("");
5389853968
for (const [endpoint, diff] of result.diffs) {
53899-
lines.push(`<details>`);
53900-
lines.push(`<summary><strong>${endpoint}</strong></summary>`);
53969+
lines.push(`**${endpoint}**`);
5390153970
lines.push("");
5390253971
lines.push("```diff");
5390353972
lines.push(diff);
5390453973
lines.push("```");
5390553974
lines.push("");
53906-
lines.push("</details>");
53907-
lines.push("");
5390853975
}
5390953976
}
5391053977
else {
@@ -53969,14 +54036,14 @@ function saveReport(report, markdown, outputDir) {
5396954036
function generatePRSummary(report) {
5397054037
const lines = [];
5397154038
if (report.diffCount === 0) {
53972-
lines.push("## ✅ MCP Conformance: All Tests Passed");
54039+
lines.push("## ✅ MCP Conformance: No Changes");
5397354040
lines.push("");
53974-
lines.push(`Tested ${report.results.length} configuration(s) - no API breaking changes detected.`);
54041+
lines.push(`Tested ${report.results.length} configuration(s) - no API changes detected.`);
5397554042
}
5397654043
else {
53977-
lines.push("## ⚠️ MCP Conformance: API Differences Detected");
54044+
lines.push("## 📋 MCP Conformance: API Changes Detected");
5397854045
lines.push("");
53979-
lines.push(`**${report.diffCount}** of ${report.results.length} configuration(s) have differences.`);
54046+
lines.push(`**${report.diffCount}** of ${report.results.length} configuration(s) have changes.`);
5398054047
lines.push("");
5398154048
lines.push("### Changed Endpoints");
5398254049
lines.push("");
@@ -54054,6 +54121,7 @@ function getInputs() {
5405454121
// Test configuration
5405554122
compareRef: getInput("compare_ref"),
5405654123
failOnError: getBooleanInput("fail_on_error") !== false, // default true
54124+
failOnDiff: getBooleanInput("fail_on_diff") === true, // default false
5405754125
envVars: getInput("env_vars"),
5405854126
serverTimeout: parseInt(getInput("server_timeout") || "30000", 10),
5405954127
};
@@ -54188,10 +54256,11 @@ async function run() {
5418854256
// Determine comparison ref
5418954257
const currentBranch = await getCurrentBranch();
5419054258
const compareRef = await determineCompareRef(inputs.compareRef, process.env.GITHUB_REF);
54259+
const compareRefDisplay = await getRefDisplayName(compareRef);
5419154260
lib_core.info("");
5419254261
lib_core.info(`📊 Comparison:`);
5419354262
lib_core.info(` Current: ${currentBranch}`);
54194-
lib_core.info(` Compare: ${compareRef}`);
54263+
lib_core.info(` Compare: ${compareRefDisplay}${compareRefDisplay !== compareRef ? ` (${compareRef.substring(0, 7)})` : ""}`);
5419554264
// Run all tests
5419654265
lib_core.info("");
5419754266
lib_core.info("🧪 Running conformance tests...");
@@ -54204,7 +54273,7 @@ async function run() {
5420454273
// Generate and save report
5420554274
lib_core.info("");
5420654275
lib_core.info("📝 Generating report...");
54207-
const report = generateReport(results, currentBranch, compareRef);
54276+
const report = generateReport(results, currentBranch, compareRefDisplay);
5420854277
const markdown = generateMarkdownReport(report);
5420954278
saveReport(report, markdown, workDir);
5421054279
// Set final status
@@ -54216,13 +54285,18 @@ async function run() {
5421654285
lib_core.setFailed(`❌ Probe errors occurred in: ${errorConfigs.join(", ")}`);
5421754286
}
5421854287
else if (report.diffCount > 0) {
54219-
lib_core.warning(`⚠️ ${report.diffCount} configuration(s) have API differences`);
54288+
if (inputs.failOnDiff) {
54289+
lib_core.setFailed(`❌ ${report.diffCount} configuration(s) have API changes`);
54290+
}
54291+
else {
54292+
lib_core.info(`📋 ${report.diffCount} configuration(s) have API changes`);
54293+
}
5422054294
if (hasErrors) {
5422154295
lib_core.warning("Some configurations had probe errors (fail_on_error is disabled)");
5422254296
}
5422354297
}
5422454298
else {
54225-
lib_core.info("✅ All conformance tests passed!");
54299+
lib_core.info("✅ All conformance tests passed - no API changes detected");
5422654300
}
5422754301
}
5422854302
catch (error) {

dist/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export interface ActionInputs {
4545
customMessages: CustomMessage[];
4646
compareRef: string;
4747
failOnError: boolean;
48+
failOnDiff: boolean;
4849
envVars: string;
4950
serverTimeout: number;
5051
httpStartCommand: string;

src/git.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,72 @@ export async function checkoutPrevious(): Promise<void> {
191191
// Ignore errors
192192
}
193193
}
194+
195+
/**
196+
* Get a display-friendly name for a ref.
197+
* Returns branch/tag name if available, otherwise the short SHA.
198+
*/
199+
export async function getRefDisplayName(ref: string): Promise<string> {
200+
// If it's already a readable name (not a SHA), return it
201+
if (!ref.match(/^[a-f0-9]{40}$/i) && !ref.match(/^[a-f0-9]{7,}$/i)) {
202+
// It's likely already a branch/tag name
203+
return ref;
204+
}
205+
206+
// Try to find a branch name pointing to this ref
207+
let output = "";
208+
try {
209+
await exec.exec("git", ["branch", "--points-at", ref, "--format=%(refname:short)"], {
210+
silent: true,
211+
listeners: {
212+
stdout: (data) => {
213+
output += data.toString();
214+
},
215+
},
216+
});
217+
const branches = output.trim().split("\n").filter(Boolean);
218+
if (branches.length > 0) {
219+
// Prefer main/master if available
220+
if (branches.includes("main")) return "main";
221+
if (branches.includes("master")) return "master";
222+
return branches[0];
223+
}
224+
} catch {
225+
// Ignore errors
226+
}
227+
228+
// Try to find a tag pointing to this ref
229+
output = "";
230+
try {
231+
await exec.exec("git", ["tag", "--points-at", ref], {
232+
silent: true,
233+
listeners: {
234+
stdout: (data) => {
235+
output += data.toString();
236+
},
237+
},
238+
});
239+
const tags = output.trim().split("\n").filter(Boolean);
240+
if (tags.length > 0) {
241+
return tags[0];
242+
}
243+
} catch {
244+
// Ignore errors
245+
}
246+
247+
// Fall back to short SHA
248+
output = "";
249+
try {
250+
await exec.exec("git", ["rev-parse", "--short", ref], {
251+
silent: true,
252+
listeners: {
253+
stdout: (data) => {
254+
output += data.toString();
255+
},
256+
},
257+
});
258+
return output.trim() || ref;
259+
} catch {
260+
return ref.substring(0, 7);
261+
}
262+
}

0 commit comments

Comments
 (0)