Repository navigation
340 lines (326 loc) · 15.3 KB
/
Copy patheverything-mcp-diff.yml
File metadata and controls
340 lines (326 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# Tracks public interface changes to the Everything MCP server.
# The Everything server is a reference implementation demonstrating all MCP features.
# This workflow helps reviewers see how tools, resources, prompts, and capabilities
# evolve over time - useful for SDK compliance validation and catching regressions.
#
# Contributed by @SamMorrowDrums in #3260 and adapted for v2/main in #4860:
#
# - The check is `npm run interface-diff` (scripts/interface-diff.mjs), which
# drives the `mcp-server-diff` CLI, an exact-pinned root devDependency. The
# same script is a `local:gate` stage, and `scripts/lib/workflow-gate.test.mjs`
# requires every npm script a workflow runs to be one, so CI and the gate run
# the same check. No third-party action runs here.
# - The base is chosen per event below rather than by the tool, whose default
# (the merge-base with origin/main) is the wrong base for PRs to v2/main.
# - Untrusted code (the PR's install, build and server) runs only in `diff`,
# which holds a read-only token and no persisted credentials. The sticky
# comment is posted by `comment`, which holds `pull-requests: write` and runs
# none of the PR's code, and only for PRs from this repository: a fork PR's
# token is read-only, so there the report is in the job summary and the
# `mcp-diff-report` artifact. `pull_request_target` is deliberately not used,
# since it would run the fork's build next to a write token. If fork PRs need
# the comment too, the way is a separate `workflow_run` workflow that posts
# the artifact without checking anything out.
#
# A changed interface does not fail the run: the diff is information for the
# reviewer. A diff that cannot run (a base that does not build, a server that
# cannot be probed) does.
#
# The Version Packages PR is pushed and opened with the workflow token, whose
# events start no `pull_request` run here: GitHub holds each one as
# `action_required`, or fails it with no jobs (#5173). So the Version Packages
# workflow dispatches this one on the PR's branch, as it does the CI workflows
# (#5110), and the `dispatch-status` jobs report the dispatched run on the PR
# as a commit status. The Prepare Python Release PR touches none of the paths
# below, so it is not dispatched there.
#
# See: https://github.com/modelcontextprotocol/inspector/issues/1034
name: Everything Server MCP Diff
on:
push:
branches: [main, v2/main]
paths:
- "src/everything/**"
- "package.json"
- "package-lock.json"
- "tsconfig.json"
- "scripts/interface-diff.mjs"
- "scripts/lib/win-shell-args.mjs"
- ".github/workflows/everything-mcp-diff.yml"
# No branch filter: PRs to v2/main and to main both run, and so does a PR
# stacked on another PR's branch. The base is the PR's own base either way.
pull_request:
paths:
- "src/everything/**"
- "package.json"
- "package-lock.json"
- "tsconfig.json"
- "scripts/interface-diff.mjs"
- "scripts/lib/win-shell-args.mjs"
- ".github/workflows/everything-mcp-diff.yml"
# A published Release (a milestone, tagged vX.Y.Z on main; see RELEASING.md)
# surfaces the cumulative interface delta since the previous Release. This
# replaces #3260's `typescript-servers-*` tag trigger: those tags stopped in
# 2024, and no release since has used them.
release:
types: [published]
# Manual trigger: the branch chosen in "Use workflow from" is the head, and
# `compare_ref` any base. #3260 also took a `target_ref`, but a target that
# predates the interface-diff script has no `npm run interface-diff` to run,
# so the head is the dispatched branch instead.
workflow_dispatch:
inputs:
compare_ref:
description: "Base ref to compare against (commit SHA, tag, or branch; defaults to the merge-base with v2/main)"
required: false
default: ""
permissions:
contents: read
# Superseded runs on the same PR are cancelled. Every other run gets a group
# of its own: a group holds one running and one pending run, so a shared group
# would drop a push's run when two more queued behind it.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
diff:
name: Diff Everything Server Interface
timeout-minutes: 15
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
# The base is built from history, so all of it is needed, tags
# included (the release base is the previous Release's tag).
fetch-depth: 0
# The event's own commit (the merge commit for a PR), not a branch
# ref that a later push could move before this step runs.
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
# A root `npm ci` runs every workspace's `prepare`, which builds it, so
# the head build the diff probes exists after this step.
- name: Install dependencies and build
run: npm ci
- name: Choose the base to compare against
id: base
env:
EVENT: ${{ github.event_name }}
PR_BASE: ${{ github.event.pull_request.base.sha }}
BEFORE: ${{ github.event.before }}
INPUT_BASE: ${{ inputs.compare_ref }}
TAG: ${{ github.event.release.tag_name }}
PUBLISHED: ${{ github.event.release.published_at }}
REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
case "$EVENT" in
# The checkout is the PR's merge commit, so its base tip is
# exactly what the PR changes.
pull_request) base="$PR_BASE" ;;
# The branch as it was before this push; a new branch has none.
push)
if [ -n "$BEFORE" ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ]; then
base="$BEFORE"
else
base=$(git rev-parse HEAD^)
fi ;;
# The Release published just before this one (so a re-run of an
# older Release still compares against ITS predecessor). Not
# `git describe`: the date-stamped releases tagged commits that
# never reached main, so the nearest ancestor tag is years older
# than the last release.
release)
base=$(gh release list --repo "$REPO" --exclude-drafts --exclude-pre-releases --limit 50 \
--json tagName,publishedAt \
--jq "map(select(.tagName != \"$TAG\" and .publishedAt < \"$PUBLISHED\")) | sort_by(.publishedAt) | last | .tagName // empty")
if [ -z "$base" ]; then echo "no earlier Release to compare against" >&2; exit 1; fi ;;
workflow_dispatch)
if [ -n "$INPUT_BASE" ]; then
base="$INPUT_BASE"
else
base=$(git merge-base HEAD origin/v2/main)
fi ;;
*) echo "unexpected event: $EVENT" >&2; exit 1 ;;
esac
echo "Comparing against $base"
echo "ref=$base" >> "$GITHUB_OUTPUT"
- name: Diff the interface
env:
BASE: ${{ steps.base.outputs.ref }}
run: npm run interface-diff -- --base "$BASE" --report-dir mcp-diff-report
- name: Write the job summary
if: always()
run: |
{
if [ -f mcp-diff-report/report.md ]; then
# A step summary holds at most 1 MiB.
head -c 900000 mcp-diff-report/report.md
if [ "$(wc -c < mcp-diff-report/report.md)" -gt 900000 ]; then
echo ""
echo "_The report is truncated here; the full report is in the run's \`mcp-diff-report\` artifact._"
fi
else
echo "The interface diff produced no report; see the job log."
fi
echo ""
echo "---"
echo ""
echo "ℹ️ **Interpreting Results**"
echo ""
echo "The Everything server is a reference implementation that exercises all MCP features."
echo "Interface changes here may indicate:"
echo "- New MCP protocol features being demonstrated"
echo "- Updated tool schemas or descriptions"
echo "- New resources, prompts, or capabilities"
echo ""
echo "Review changes to ensure they align with the intended protocol updates."
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload the report
if: always()
uses: actions/upload-artifact@v7
with:
name: mcp-diff-report
path: mcp-diff-report/
if-no-files-found: ignore
# A dispatched run's checks are left out of the status rollup a pull request
# reads, so on a dispatch these two jobs mirror the run onto the commit as a
# status, as in typescript.yml (whose comments hold the reasoning): pending
# at the start, then the result. Every other event skips both. Unlike
# typescript.yml's (#5223), the pending job also stays silent when a newer
# dispatched run exists, so a late or re-run older run cannot reset the
# newer run's result to pending, which its own result job would then leave.
dispatch-status-pending:
name: Report the dispatched run as pending
if: github.event_name == 'workflow_dispatch'
timeout-minutes: 5
runs-on: ubuntu-latest
permissions:
statuses: write
# To look for a newer dispatched run of this workflow on the commit.
actions: read
steps:
- name: Set the commit status to pending
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
# A re-run keeps its run ID, so a newer run means this one is
# superseded and the status is no longer its to set.
NEWER=$(gh run list --workflow everything-mcp-diff.yml --event workflow_dispatch \
--commit "$GITHUB_SHA" --json databaseId \
--jq "[.[] | select(.databaseId > $GITHUB_RUN_ID)] | length") || NEWER=0
if [ "${NEWER:-0}" -gt 0 ]; then
echo "Superseded by a newer dispatched run, which reports instead"
exit 0
fi
gh api "repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
-f state=pending -f context="Everything diff (dispatched)" \
-f description="Dispatched run in progress" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
# `comment` runs only on `pull_request`, so on a dispatch it is skipped,
# which counts as passed; the report is in the run's summary and artifact.
dispatch-status:
name: Report the dispatched run's result
if: github.event_name == 'workflow_dispatch' && always()
needs: [dispatch-status-pending, diff, comment]
timeout-minutes: 5
runs-on: ubuntu-latest
permissions:
statuses: write
# To look for a newer dispatched run of this workflow on the commit.
actions: read
steps:
- name: Set the commit status to the run's result
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NEEDS: ${{ toJSON(needs) }}
run: |
# A run superseded by a newer dispatch on the same commit posts
# nothing, whatever its jobs' results: the newer run owns the status
# (#5178). A failed lookup counts as no newer run, so this run still
# reports: a stale status is better than one left pending.
NEWER=$(gh run list --workflow everything-mcp-diff.yml --event workflow_dispatch \
--commit "$GITHUB_SHA" --json databaseId \
--jq "[.[] | select(.databaseId > $GITHUB_RUN_ID)] | length") || NEWER=0
if [ "${NEWER:-0}" -gt 0 ]; then
echo "Superseded by a newer dispatched run, which reports instead"
exit 0
fi
if jq -e 'all(.[]; .result == "success" or .result == "skipped")' <<<"$NEEDS" >/dev/null; then
STATE=success DESCRIPTION="The diff ran; the report is in the run's summary"
elif jq -e 'any(.[]; .result == "failure")' <<<"$NEEDS" >/dev/null; then
STATE=failure DESCRIPTION="The diff could not run; open the run for details"
else
STATE=error DESCRIPTION="The run was cancelled; dispatch it again"
fi
gh api "repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
-f state="$STATE" -f context="Everything diff (dispatched)" \
-f description="$DESCRIPTION" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
# Post (or update) a sticky PR comment so reviewers can see the diff without
# navigating to the Actions tab. One comment per PR, found again by its
# marker and edited on each push. This job checks nothing out and runs none
# of the PR's code; it only reads the report the `diff` job uploaded.
# Dependabot's PRs are same-repo but get a read-only token, so they are
# skipped like fork PRs and keep the job summary and the artifact.
comment:
name: Post the diff on the PR
needs: diff
if: >-
always() &&
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.login != 'dependabot[bot]' &&
(needs.diff.result == 'success' || needs.diff.result == 'failure')
timeout-minutes: 5
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Download the report
uses: actions/download-artifact@v8
continue-on-error: true
with:
name: mcp-diff-report
path: mcp-diff-report
- name: Post / update sticky PR comment
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
marker='<!-- everything-mcp-diff -->'
{
echo "$marker"
if [ -f mcp-diff-report/report.md ]; then
# A comment holds at most 65536 characters.
head -c 60000 mcp-diff-report/report.md
if [ "$(wc -c < mcp-diff-report/report.md)" -gt 60000 ]; then
echo ""
echo "_The report is truncated here; the full report is in the run's \`mcp-diff-report\` artifact._"
fi
else
echo "## \`everything\`: MCP interface diff"
echo ""
echo "❌ The diff job produced no report; see the run's log."
fi
echo ""
echo "<sub>Updated by [this run]($RUN_URL) for \`$HEAD_SHA\`.</sub>"
} > body.md
id=$(gh api --paginate "repos/$REPO/issues/$PR/comments" \
--jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | .id" | head -n 1)
if [ -n "$id" ]; then
gh api -X PATCH "repos/$REPO/issues/comments/$id" -F body=@body.md > /dev/null
else
gh api "repos/$REPO/issues/$PR/comments" -F body=@body.md > /dev/null
fi