Skip to content

Commit 7c33772

Browse files
authored
Merge branch 'main' into fix-sampling-with-tools-client-validation
2 parents eb7aae5 + 2bb7f47 commit 7c33772

19 files changed

Lines changed: 971 additions & 18 deletions

.changeset/busy-weeks-hang.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modelcontextprotocol/core': patch
3+
'@modelcontextprotocol/server': patch
4+
---
5+
6+
Fix ReDoS vulnerability in UriTemplate regex patterns (CVE-2026-0621)

.changeset/cyan-cycles-pump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/server': patch
3+
---
4+
5+
missing change for fix(client): replace body.cancel() with text() to prevent hanging

.changeset/strong-hairs-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/server': patch
3+
---
4+
5+
add application/json header for notifications

.github/workflows/conformance.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Conformance Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
client-conformance:
14+
runs-on: ubuntu-latest
15+
continue-on-error: true # Non-blocking initially
16+
steps:
17+
- uses: actions/checkout@v4
18+
- name: Install pnpm
19+
uses: pnpm/action-setup@v4
20+
with:
21+
run_install: false
22+
- uses: actions/setup-node@v4
23+
with:
24+
node-version: 24
25+
cache: pnpm
26+
cache-dependency-path: pnpm-lock.yaml
27+
- run: pnpm install
28+
- run: pnpm run build:all
29+
- run: pnpm run test:conformance:client:all

.github/workflows/release-v1x.yml

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Publish v1.x releases when a v1.* tag is pushed
2+
# This allows releasing from the v1.x branch without switching GitHub's default branch
3+
#
4+
# Automatic (latest v1.x):
5+
# git checkout v1.x
6+
# npm version patch # bumps version and creates tag
7+
# git push origin v1.x --tags
8+
#
9+
# Manual (for older minor versions like v1.23.x):
10+
# 1. Create a release branch: git checkout -b release/1.23 v1.23.1
11+
# 2. Apply your fixes
12+
# 3. Bump version: npm version patch # creates v1.23.2 tag
13+
# 4. Push: git push origin release/1.23 --tags
14+
# 5. Run this workflow manually from GitHub Actions, specifying the tag
15+
#
16+
# The workflow will automatically build, test, and publish to npm.
17+
18+
name: Publish v1.x
19+
20+
on:
21+
push:
22+
tags:
23+
- 'v1.*'
24+
workflow_dispatch:
25+
inputs:
26+
ref:
27+
description: 'Git ref to release (tag, e.g., v1.23.2)'
28+
required: true
29+
type: string
30+
31+
permissions:
32+
contents: read
33+
34+
concurrency: ${{ github.workflow }}-${{ inputs.ref || github.ref }}
35+
36+
jobs:
37+
build:
38+
runs-on: ubuntu-latest
39+
40+
steps:
41+
- uses: actions/checkout@v4
42+
with:
43+
ref: ${{ inputs.ref || github.ref }}
44+
- uses: actions/setup-node@v4
45+
with:
46+
node-version: 24
47+
cache: npm
48+
49+
- run: npm ci
50+
- run: npm run check
51+
- run: npm run build
52+
53+
test:
54+
runs-on: ubuntu-latest
55+
strategy:
56+
fail-fast: false
57+
matrix:
58+
node-version: [18, 24]
59+
60+
steps:
61+
- uses: actions/checkout@v4
62+
with:
63+
ref: ${{ inputs.ref || github.ref }}
64+
- uses: actions/setup-node@v4
65+
with:
66+
node-version: ${{ matrix.node-version }}
67+
cache: npm
68+
69+
- run: npm ci
70+
- run: npm test
71+
72+
publish:
73+
runs-on: ubuntu-latest
74+
needs: [build, test]
75+
environment: release
76+
77+
permissions:
78+
contents: read
79+
id-token: write
80+
81+
steps:
82+
- uses: actions/checkout@v4
83+
with:
84+
ref: ${{ inputs.ref || github.ref }}
85+
- uses: actions/setup-node@v4
86+
with:
87+
node-version: 24
88+
cache: npm
89+
registry-url: 'https://registry.npmjs.org'
90+
91+
- run: npm ci
92+
93+
- name: Determine npm tag
94+
id: npm-tag
95+
run: |
96+
VERSION=$(node -p "require('./package.json').version")
97+
# Use "release-X.Y" as tag for v1.x releases
98+
# This allows users to install specific minor versions:
99+
# npm install @modelcontextprotocol/sdk@release-1.25
100+
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
101+
echo "tag=release-${MAJOR_MINOR}" >> $GITHUB_OUTPUT
102+
103+
- run: npm publish --provenance --access public --tag ${{ steps.npm-tag.outputs.tag }}
104+
env:
105+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

CONTRIBUTING.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,48 @@ pnpm --filter @modelcontextprotocol/examples-server exec tsx src/simpleStreamabl
6060
pnpm --filter @modelcontextprotocol/examples-client exec tsx src/simpleStreamableHttp.ts
6161
```
6262

63+
## Releasing v1.x Patches
64+
65+
The `v1.x` branch contains the stable v1 release. To release a patch:
66+
67+
### Latest v1.x (e.g., v1.25.3)
68+
69+
```bash
70+
git checkout v1.x
71+
git pull origin v1.x
72+
# Apply your fix or cherry-pick commits
73+
npm version patch # Bumps version and creates tag (e.g., v1.25.3)
74+
git push origin v1.x --tags
75+
```
76+
77+
The tag push automatically triggers the release workflow.
78+
79+
### Older minor versions (e.g., v1.23.2)
80+
81+
For patching older minor versions that aren't on the `v1.x` branch:
82+
83+
```bash
84+
# 1. Create a release branch from the last release tag
85+
git checkout -b release/1.23 v1.23.1
86+
87+
# 2. Apply your fixes (cherry-pick or manual)
88+
git cherry-pick <commit-hash>
89+
90+
# 3. Bump version and push
91+
npm version patch # Creates v1.23.2 tag
92+
git push origin release/1.23 --tags
93+
```
94+
95+
Then manually trigger the "Publish v1.x" workflow from [GitHub Actions](https://github.com/modelcontextprotocol/typescript-sdk/actions/workflows/release-v1x.yml), specifying the tag (e.g., `v1.23.2`).
96+
97+
### npm Tags
98+
99+
v1.x releases are published with `release-X.Y` npm tags (e.g., `release-1.25`), not `latest`. To install a specific minor version:
100+
101+
```bash
102+
npm install @modelcontextprotocol/sdk@release-1.25
103+
```
104+
63105
## Code of Conduct
64106

65107
This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). Please review it before contributing.

package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,18 @@
2929
"lint:all": "pnpm -r lint",
3030
"lint:fix:all": "pnpm -r lint:fix",
3131
"check:all": "pnpm -r typecheck && pnpm -r lint",
32-
"test:all": "pnpm -r test"
32+
"test:all": "pnpm -r test",
33+
"test:conformance:client": "conformance client --command 'npx tsx src/conformance/everything-client.ts'",
34+
"test:conformance:client:all": "conformance client --command 'npx tsx src/conformance/everything-client.ts' --suite all",
35+
"test:conformance:client:run": "npx tsx src/conformance/everything-client.ts"
3336
},
3437
"devDependencies": {
3538
"@cfworker/json-schema": "catalog:runtimeShared",
3639
"@changesets/changelog-github": "^0.5.2",
3740
"@changesets/cli": "^2.29.8",
3841
"@eslint/js": "catalog:devTools",
42+
"@modelcontextprotocol/client": "workspace:^",
43+
"@modelcontextprotocol/conformance": "0.1.9",
3944
"@types/content-type": "catalog:devTools",
4045
"@types/cors": "catalog:devTools",
4146
"@types/cross-spawn": "catalog:devTools",
@@ -56,7 +61,8 @@
5661
"typescript": "catalog:devTools",
5762
"typescript-eslint": "catalog:devTools",
5863
"vitest": "catalog:devTools",
59-
"ws": "catalog:devTools"
64+
"ws": "catalog:devTools",
65+
"zod": "catalog:runtimeShared"
6066
},
6167
"resolutions": {
6268
"strip-ansi": "6.0.1"

packages/client/src/client/auth.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -670,12 +670,12 @@ export async function discoverOAuthProtectedResourceMetadata(
670670
});
671671

672672
if (!response || response.status === 404) {
673-
await response?.body?.cancel();
673+
await response?.text?.().catch(() => {});
674674
throw new Error(`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`);
675675
}
676676

677677
if (!response.ok) {
678-
await response.body?.cancel();
678+
await response.text?.().catch(() => {});
679679
throw new Error(`HTTP ${response.status} trying to load well-known OAuth protected resource metadata.`);
680680
}
681681
return OAuthProtectedResourceMetadataSchema.parse(await response.json());
@@ -803,12 +803,12 @@ export async function discoverOAuthMetadata(
803803
});
804804

805805
if (!response || response.status === 404) {
806-
await response?.body?.cancel();
806+
await response?.text?.().catch(() => {});
807807
return undefined;
808808
}
809809

810810
if (!response.ok) {
811-
await response.body?.cancel();
811+
await response.text?.().catch(() => {});
812812
throw new Error(`HTTP ${response.status} trying to load well-known OAuth metadata`);
813813
}
814814

@@ -918,7 +918,7 @@ export async function discoverAuthorizationServerMetadata(
918918
}
919919

920920
if (!response.ok) {
921-
await response.body?.cancel();
921+
await response.text?.().catch(() => {});
922922
// Continue looking for any 4xx response code.
923923
if (response.status >= 400 && response.status < 500) {
924924
continue; // Try next URL

packages/client/src/client/sse.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export class SSEClientTransport implements Transport {
260260

261261
const response = await (this._fetch ?? fetch)(this._endpoint, init);
262262
if (!response.ok) {
263-
const text = await response.text().catch(() => null);
263+
const text = await response.text?.().catch(() => null);
264264

265265
if (response.status === 401 && this._authProvider) {
266266
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
@@ -285,7 +285,7 @@ export class SSEClientTransport implements Transport {
285285
}
286286

287287
// Release connection - POST responses don't have content we need
288-
await response.body?.cancel();
288+
await response.text?.().catch(() => {});
289289
} catch (error) {
290290
this.onerror?.(error as Error);
291291
throw error;

packages/client/src/client/streamableHttp.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export class StreamableHTTPClientTransport implements Transport {
235235
});
236236

237237
if (!response.ok) {
238-
await response.body?.cancel();
238+
await response.text?.().catch(() => {});
239239

240240
if (response.status === 401 && this._authProvider) {
241241
// Need to authenticate
@@ -495,7 +495,7 @@ export class StreamableHTTPClientTransport implements Transport {
495495
}
496496

497497
if (!response.ok) {
498-
const text = await response.text().catch(() => null);
498+
const text = await response.text?.().catch(() => null);
499499

500500
if (response.status === 401 && this._authProvider) {
501501
// Prevent infinite recursion when server returns 401 after successful auth
@@ -568,7 +568,7 @@ export class StreamableHTTPClientTransport implements Transport {
568568

569569
// If the response is 202 Accepted, there's no body to process
570570
if (response.status === 202) {
571-
await response.body?.cancel();
571+
await response.text?.().catch(() => {});
572572
// if the accepted notification is initialized, we start the SSE stream
573573
// if it's supported by the server
574574
if (isInitializedNotification(message)) {
@@ -603,12 +603,12 @@ export class StreamableHTTPClientTransport implements Transport {
603603
this.onmessage?.(msg);
604604
}
605605
} else {
606-
await response.body?.cancel();
606+
await response.text?.().catch(() => {});
607607
throw new StreamableHTTPError(-1, `Unexpected content type: ${contentType}`);
608608
}
609609
} else {
610610
// No requests in message but got 200 OK - still need to release connection
611-
await response.body?.cancel();
611+
await response.text?.().catch(() => {});
612612
}
613613
} catch (error) {
614614
this.onerror?.(error as Error);
@@ -647,7 +647,7 @@ export class StreamableHTTPClientTransport implements Transport {
647647
};
648648

649649
const response = await (this._fetch ?? fetch)(this._url, init);
650-
await response.body?.cancel();
650+
await response.text?.().catch(() => {});
651651

652652
// We specifically handle 405 as a valid response according to the spec,
653653
// meaning the server does not support explicit session termination

0 commit comments

Comments
 (0)