Skip to content

Commit 672f9f4

Browse files
authored
Merge pull request #1993 from modelcontextprotocol/v2/chore/milestone-merge-v2.2.0
chore: merge v2/main for the v2.2.0 milestone release
2 parents c7bccd4 + ec28099 commit 672f9f4

119 files changed

Lines changed: 8840 additions & 2824 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/copilot-instructions.md‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ Both exist and do different jobs. Theme files (`src/theme/<Component>.ts`) custo
7272
- `src/types/` is only for ambient `.d.ts` module stubs — not a home for new domain types.
7373
- ⚠️ The coverage `include` is a **whitelist** naming `components` / `hooks` / `theme` / `lib` / `utils` / `server` (plus the `core/*` runtime). A module placed outside those directories silently falls out of the ≥90% gate. Flag new top-level files or new grab-bag directories.
7474

75+
## Dependency placement
76+
77+
- **The MCP SDK packages (`@modelcontextprotocol/client`, `core`, `server`, `server-legacy`, `ext-apps`) belong in the root `package.json` only.** Adding one to a `clients/*/package.json` is a review finding: Node resolution walks up, so the root install already serves every client, and a per-client entry installs a second copy that drifts (it produced two versions of `ext-apps`, and of the transitive v1 SDK, at once — #1970) and reintroduces the duplicate-copy failure `vitest.shared.mts` has a `dedupe` workaround for.
78+
- **The v1 `@modelcontextprotocol/sdk` is not a dependency of this repo** and must not become one. It is a peer of `ext-apps`, present in lock files only.
79+
- Dependencies reached only through **root-owned code with no manifest** (`test-servers/src`, `core/`) are declared at the root and aliased to the **repo root** in `vitest.shared.mts` — as `express` and `yaml` are — not to `<client>/node_modules` like the other pins there.
80+
- **A dependency that renders React components must be bundled into the client that uses it, and is then not a root dependency.** An externalized package resolves its own `react` from wherever npm placed it in the *consumer's* tree, beside a React satisfying *its* peer range — looser than ours, which is all it takes to split React. `ink-form`/`ink-scroll-view` declare `">=18"`, so a consumer's React 18 satisfies them, the TUI ends up with two React instances, and it crashes on the first hook (#1952). Both are inlined via `noExternal` in `clients/tui/tsup.config.ts`. **`ink` is the one exemption, justified by cost (~1.4MB) — never by a peer range**: flag any claim that `">=19"` keeps npm from misplacing it, which is false and was in this repo once. What keeps it safe is the **root `react` range staying open to the whole major (`^19.0.0`)** so npm can dedupe with a consumer's pinned React 19; treat narrowing that range as reopening the bug. `clients/tui/__tests__/tsupConfig.test.ts` enforces the split, the root-declaration of exempt packages, and that range.
81+
- **Which section is a separate question from which manifest.** A package `core/` imports at runtime must be in root **`dependencies`**: client builds externalize npm packages, so a published install resolves them from the root manifest and devDependencies are absent there. Only test/build-only packages (`express`) belong in `devDependencies`. Flag a runtime `core/` import added to `devDependencies` — it passes every local check and breaks the published package.
82+
7583
## Tests and the coverage gate
7684

7785
- **All new or modified code needs tests.** The per-file gate is **≥ 90% on all four dimensions** — lines, statements, functions, **and branches** — enforced in CI for `clients/{web,cli,tui,launcher}` and the gated `core/` runtime.
@@ -85,15 +93,18 @@ Both exist and do different jobs. Theme files (`src/theme/<Component>.ts`) custo
8593

8694
### Rendering components in tests
8795

88-
- **Always render through `renderWithMantine`** from `src/test/renderWithMantine.tsx`. Never hand-roll a bare `MantineProvider` — that reintroduces a real failure class where a `Transition`/`Modal` timer fires after happy-dom tears down `window` and fails the entire run.
96+
- **Always render through `renderWithMantine`** from `src/test/renderWithMantine.tsx`. A hand-rolled bare `MantineProvider` skips the project theme and the helper's options, and drifts from every other test. (It does *not* reintroduce the timer-leak class — an older version of this rule said so; the leaked-timer net in `setup.ts` is global and covers every unit test regardless of how it renders.)
8997
- For a forced color scheme, pass the option — `renderWithMantine(ui, { colorScheme: "dark" })` — rather than a hand-rolled `defaultColorScheme` provider.
9098
- Only when asserting _mid-flight_ transition state, use `renderWithMantineTransitions`, passing `settleMs` derived from the component's real animation duration. Do **not** combine it with `vi.useFakeTimers()`, and use the `unmount()` it returns if the test unmounts the tree itself.
9199

92100
## Gates and PR hygiene
93101

94102
- `npm run format` before committing; **`npm run ci` before pushing** (`validate` → `coverage` → `verify:build-gate` → `smoke` → Storybook). `npm run validate` is the fast inner-loop check and is **not** a substitute — it runs `test`, not `test:coverage`, so it does zero coverage gating.
103+
- **A dependency bump must land in every install that declares it.** v2 is not a workspace — the root and each `clients/*` have their own `node_modules`, and a client's test project compiles `core/` and `test-servers/src` (which resolve from the **root**) alongside the client's own sources. Bumping a shared dependency in one manifest only puts two versions of it in one `tsc` program; for a recursive-generic surface like zod that exhausts the tsc heap (#1896). `verify:dep-lockstep` fails the build on this, so a PR bumping a package that the shared sources import should update the root **and every client that already lists it** — not every client unconditionally, since a package absent from an install can't skew and adding it there would be a spurious dependency.
104+
- **A test or smoke must not touch real user state.** The web smokes run against a throwaway catalog via the shared `scripts/lib/prod-web-server.mjs` helper, never the developer's `~/.mcp-inspector/mcp.json` (#1977); the cli/tui smokes drive a temp `--catalog`. A new smoke spawning its own server, or teardown that removes a work dir without first awaiting `stopChild` (the #1801 race — `child-cleanup.mjs` exports both halves and both are required), should be flagged.
95105
- **Every PR references an issue**, first body line `Closes #<ISSUE_NUMBER>`.
96106
- **Every PR carries exactly one version label**, `v1` or `v2`, matching its base branch.
107+
- **Commits carry a `Signed-off-by:` trailer.** The DCO check is a hard merge gate and fails on a single unsigned commit; it matches the trailer against the author *or* committer, and skips only merge and bot commits. Use `git commit -s` — note `format.signOff` does *not* sign `git commit` (only `format-patch`). Repairing pushed commits means `git rebase HEAD~<n> --signoff` + `git push --force-with-lease`; remediation commits are not enabled on this repo.
97108
- Update the relevant `README.md` / `AGENTS.md` when a change adds, removes, renames, or repurposes a file or folder, changes the structure or tech stack, or introduces a command, dependency, or architectural pattern.
98109

99110
## What to prioritize in review

‎.github/workflows/claude.yml‎

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,18 @@ jobs:
3535
id-token: write
3636
actions: read
3737
steps:
38+
# Actions are pinned to full commit SHAs rather than mutable major tags:
39+
# this job holds ANTHROPIC_API_KEY and grants the agent Bash, so a
40+
# force-moved tag would be an unreviewed code change inside a
41+
# secret-holding job. The trailing `# vX.Y.Z` comment is the form
42+
# Dependabot reads, so pinning costs us no upgrade automation. (#1882)
3843
- name: Get PR details
3944
if: |
4045
(github.event_name == 'issue_comment' && github.event.issue.pull_request) ||
4146
github.event_name == 'pull_request_review_comment' ||
4247
github.event_name == 'pull_request_review'
4348
id: pr
44-
uses: actions/github-script@v8
49+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
4550
with:
4651
script: |
4752
let prNumber;
@@ -57,26 +62,58 @@ jobs:
5762
pull_number: prNumber
5863
});
5964
65+
// A fork PR's head lives in a different repository — or in none at
66+
// all, if the fork was deleted after the PR was opened (`head.repo`
67+
// is then null). Neither is ours to check out, so both count as a
68+
// fork here and the steps below decline rather than guess.
69+
const headRepo = pr.data.head.repo?.full_name ?? null;
70+
const isFork = headRepo !== `${context.repo.owner}/${context.repo.repo}`;
71+
6072
core.setOutput('sha', pr.data.head.sha);
61-
core.setOutput('repo', pr.data.head.repo.full_name);
73+
core.setOutput('is_fork', String(isFork));
74+
75+
# A fork PR's head is untrusted code, and checking it out would put it in
76+
# reach of a tool-enabled agent run holding ANTHROPIC_API_KEY. Reviewing
77+
# the base tree instead would only trade that for a confident review of
78+
# the wrong tree, so decline visibly and leave the reason in the run.
79+
- name: Decline fork PR
80+
if: steps.pr.outcome == 'success' && steps.pr.outputs.is_fork == 'true'
81+
run: |
82+
{
83+
echo "### Claude Code declined this pull request"
84+
echo
85+
echo "The head branch lives in a fork, so its code is not checked out and Claude is not run."
86+
echo "Push the branch to this repository and re-trigger if a review is needed."
87+
} >> "$GITHUB_STEP_SUMMARY"
6288
89+
# No `repository:` — a same-repo head is all that reaches this step, and
90+
# checkout defaults to `github.repository`, which no PR can influence.
6391
- name: Checkout PR branch
64-
if: steps.pr.outcome == 'success'
65-
uses: actions/checkout@v6
92+
if: steps.pr.outcome == 'success' && steps.pr.outputs.is_fork == 'false'
93+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
6694
with:
6795
ref: ${{ steps.pr.outputs.sha }}
68-
repository: ${{ steps.pr.outputs.repo }}
6996
fetch-depth: 0
7097

98+
# `skipped` means the trigger was an issue or a non-PR comment, so there
99+
# is no head to check out and the base tree is the right one. The lookup
100+
# having *failed* is deliberately not included: this condition carries no
101+
# status-check function, so GitHub applies an implicit `success()` and
102+
# skips the step after a failed prior step anyway. Spelling out `skipped`
103+
# keeps that from having to be re-derived by the next reader.
71104
- name: Checkout repository
72-
if: steps.pr.outcome != 'success'
73-
uses: actions/checkout@v6
105+
if: steps.pr.outcome == 'skipped'
106+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
74107
with:
75108
fetch-depth: 0
76109

77110
- name: Run Claude Code
111+
# Runs only against a tree this workflow actually checked out: the base
112+
# repo (non-PR trigger) or a same-repo PR head. A fork PR reaches here
113+
# with `is_fork == 'true'` and both disjuncts false, so it is skipped.
114+
if: steps.pr.outcome == 'skipped' || steps.pr.outputs.is_fork == 'false'
78115
id: claude
79-
uses: anthropics/claude-code-action@v1
116+
uses: anthropics/claude-code-action@5ef2e550a465a721f4f45e4a7d3c340c873e1dcc # v1.0.190
80117
with:
81118
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
82119

‎.github/workflows/main.yml‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,11 @@ jobs:
9393
# CLI/web). Not part of any client's `validate`: it needs the
9494
# cli/tui/launcher bundles, which `validate` above already built
9595
# (smoke:web builds clients/web/dist on demand — #1486). smoke:web:browser
96-
# boots the prod web bundle in headless chromium (#1615). smoke:tui
97-
# self-skips here — the Ink TUI needs a real TTY (raw mode) that headless
98-
# CI lacks, so its boot/render check is local-only.
96+
# boots the prod web bundle in headless chromium (#1615); smoke:web:app
97+
# goes further and drives connect → open app → widget ready against a
98+
# composable MCP App server (#1859). Both reuse the chromium installed
99+
# above. smoke:tui self-skips here — the Ink TUI needs a real TTY (raw
100+
# mode) that headless CI lacks, so its boot/render check is local-only.
99101
run: npm run smoke
100102

101103
- name: Run Storybook play-function tests

0 commit comments

Comments
 (0)