Compare commits

...
Author SHA1 Message Date
Jeff Handley
2b7fd35fbe
Release v1.4.1 (#1694) 2026-07-08 20:30:43 -04:00
Jeff Handley
b088dd3850 Prepare release v1.4.1
- Bump version from 1.4.0 to 1.4.1
- Add release-notes link to src/PACKAGE.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-08 01:55:54 -04:00
Jeff Handley
0021d8875d
[release/1.x] Release SSE response stream reference when GET request ends (#1628) 2026-07-07 15:36:02 -04:00
Copilot
25f080115d Apply review feedback: replace _getHttpResponseCompleted with _disposed; rewrite test
- Remove _getHttpResponseCompleted and rely on _httpSseWriter being null as the signal that the GET response stream has been released. Null out _httpSseWriter in DisposeAsync so SendMessageAsync correctly skips writing post-dispose.
- Replace the dispose-idempotency role of the old flag with a dedicated _disposed bool.
- Move the explanatory comment in SendMessageAsync onto the event store branch (which intentionally still runs when the response stream is gone, to support Last-Event-ID replay).
- Rewrite the unit test to assert observable behavior via a recording Stream instead of reflecting on private fields.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-05 16:52:49 -07:00
Joel Forsyth
4d011995d1 Release SSE response stream reference when GET request ends
StreamableHttpServerTransport held a reference to the Kestrel SSE
response stream via _httpSseWriter for the entire session lifetime.
Since the transport is only disposed when the session itself is disposed
(via explicit DELETE or idle timeout), clients that disconnect without
sending DELETE — which is typical for long-lived SSE connections — left
the response stream pinned for the remainder of the idle timeout window.

Because the Kestrel HTTP connection (and its associated memory pool
buffers, Pipe readers/writers, socket send/receive buffers) is only
collectible once all references to the response stream are released,
this pinned ~20MiB of unmanaged memory per disconnected session until
cleanup. Under steady connect/disconnect churn (e.g., IDE availability
probes) this accumulated into sustained memory growth that eventually
OOMKilled the container.

Fix: release _httpSseWriter from within HandleGetRequestAsync's finally
block so the reference is dropped as soon as the GET request exits, not
just when the session is disposed. SendMessageAsync handles the
now-nullable field via the existing _getHttpResponseCompleted gate, and
any server-to-client messages sent after disconnect are still captured
by the event store writer for replay via Last-Event-ID.

Add a unit test that verifies _httpSseWriter is null once
HandleGetRequestAsync returns.

Relates to #766.
2026-06-05 16:52:49 -07:00
Jeff Handley
06e3604dd1
[release/1.x]: Add Enterprise Managed Authorization (SEP-990) support (#1305) (#1625) 2026-06-04 08:52:00 -07:00
Aniket Anil Kumar
96a3ddff69 feat: Add Enterprise Managed Authorization (SEP-990) support (#1305) 2026-06-03 16:34:59 -07:00
Jeff Handley
d7faa6b064
Release v1.4.0 (#1622) 2026-06-03 16:01:36 -07:00
Jeff Handley
3286450528 Bump version to 1.4.0 2026-06-03 02:31:25 -07:00
Stephen Halter
453132009a Validate user on Streamable HTTP session DELETE (#1604) 2026-06-03 02:00:35 -07:00
Tarek Mahmoud Sayed
55646c3ad7 Fix broken links to MCP Tasks specification (#1594)
Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com>
2026-06-03 01:59:06 -07:00
Copilot
8fdfe581b6 Stop logging stdio transport environment variables (#1538) 2026-06-03 01:58:52 -07:00
Stephen Halter
d34fac48d5 Add InheritEnvironmentVariables to StdioClientTransportOptions (#1563) 2026-06-03 01:58:35 -07:00
Jeff Handley
4498146a83 Update release processes to support release servicing branches (#1620)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-03 01:50:51 -07:00
41 changed files with 2565 additions and 134 deletions

36
.github/release-process.md vendored Normal file
View file

@ -0,0 +1,36 @@
# Release Process
The following process is used when publishing new releases to NuGet.org.
## 1. Ensure the CI workflow is fully green
- Some integration tests are flaky and may require re-running
- Once the state of the branch is known to be good, a release can proceed
- **The release workflow _does not_ run tests** — CI must be green before starting
## 2. Prepare the release
From a local clone of the repository, use Copilot CLI to invoke the `prepare-release` skill. The skill assesses the semantic version, bumps the version in [`src/Directory.Build.props`](../src/Directory.Build.props), runs API compatibility checks, reviews documentation, drafts release notes, and creates a pull request with all release artifacts.
Review the PR, request changes if needed, and merge when ready.
## 3. Publish the release
After the prepare-release PR is merged, invoke the `publish-release` skill. The skill checks for any late-arriving PRs that could affect the release, refreshes the release notes, and creates a **draft** GitHub release.
Review the draft release on GitHub, check 'Set as a pre-release' if appropriate, and click 'Publish release'.
## Branching
The `main` branch is the next-MAJOR preview and development line; currently, it produces the `2.0.0-preview.*` series. Nightly `cron` CI on `main` publishes CI-suffixed packages to GitHub Packages.
Long-lived `release/{MAJOR}.x` branches are created on demand when a shipped MAJOR needs servicing releases. Every push to a `release/*` branch publishes a CI-suffixed package to GitHub Packages, so servicing CI packages are commit-driven rather than clock-driven.
Short-lived `release-{version}` branches are local prepare-release work branches that become pull requests, such as `release-2.0.0-preview.1` or `release-1.3.1`.
Official NuGet.org publishes occur only when a GitHub Release is created from a branch's tag.
The prepare-release skill asks for the source/base branch first so the release PR targets the same line it assessed.
For the agent-facing, structured version of these rules, see [release-branches.md](skills/shared-resources/release-branches.md).
## 4. Monitor the Release workflow
- After publishing, a workflow will produce build artifacts and publish the NuGet packages to NuGet.org
- If the job fails, troubleshoot and re-run the workflow as needed
- Verify the package version becomes listed at [nuget.org/packages/ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol)

View file

@ -8,18 +8,21 @@ compatibility: Requires gh CLI with repo access for creating branches and pull r
Assess and bump the SDK version in `src/Directory.Build.props` to prepare for the next release. This skill owns the [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) assessment logic — the [SemVer assessment guide](references/semver-assessment.md) is the single source of truth for version assessment criteria used across the release workflow by both the **prepare-release** and **publish-release** skills. Assess and bump the SDK version in `src/Directory.Build.props` to prepare for the next release. This skill owns the [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) assessment logic — the [SemVer assessment guide](references/semver-assessment.md) is the single source of truth for version assessment criteria used across the release workflow by both the **prepare-release** and **publish-release** skills.
Use the shared [release branch reference](../shared-resources/release-branches.md) for branch roles, previous-release lookup rules, and release work-branch naming.
> **Note**: For comprehensive release preparation — including ApiCompat/ApiDiff, documentation review, and release notes — use the **prepare-release** skill, which incorporates version assessment as part of its broader workflow. > **Note**: For comprehensive release preparation — including ApiCompat/ApiDiff, documentation review, and release notes — use the **prepare-release** skill, which incorporates version assessment as part of its broader workflow.
## Process ## Process
### Step 1: Read Current Version and Previous Release ### Step 1: Read Current Version and Previous Release
Read `src/Directory.Build.props` on the default branch and extract: Read `src/Directory.Build.props` on the current branch and extract:
- `<VersionPrefix>` — the `MAJOR.MINOR.PATCH` version - `<VersionPrefix>` — the `MAJOR.MINOR.PATCH` version
- `<VersionSuffix>` — the prerelease suffix, when present
Display the current version to the user. The candidate version is `{VersionPrefix}` plus `-{VersionSuffix}` when the suffix is present (for example, `2.0.0-preview.1`). Display the current candidate version to the user.
Determine the previous release tag from `gh release list` (most recent **published** release). Draft releases must be ignored — they represent a pending release that has not yet shipped. Use `--exclude-drafts` or filter to only published releases when querying. Determine the previous release tag from `gh release list` (most recent **published** release). Draft releases must be ignored — they represent a pending release that has not yet shipped. Use `--exclude-drafts` or filter to only published releases when querying. The lookup is branch-aware: from a `release/{MAJOR}.x` branch, restrict candidates to tags matching `v{MAJOR}.*`; from `main`, use the most recent published release globally. See [release-branches.md](../shared-resources/release-branches.md) for details.
### Step 2: Assess and Determine Next Version ### Step 2: Assess and Determine Next Version
@ -40,26 +43,31 @@ When context about queued changes is available or can be gathered, assess the ve
#### Default Suggestion (Fallback) #### Default Suggestion (Fallback)
When a quick bump is needed without full change analysis, suggest the next **minor** version: When a quick bump is needed without full change analysis, suggest based on the candidate version:
- Current `1.0.0` → suggest `1.1.0` - **Stable candidate** — suggest the next **minor** version:
- Current `1.2.3` → suggest `1.3.0` - Current `1.0.0` → suggest `1.1.0`
- Current `1.2.3` → suggest `1.3.0`
- **Prerelease candidate** — if the suffix starts with an identifier such as `preview.` or `rc.` followed by an integer, suggest incrementing the trailing integer:
- Current `2.0.0-preview.1` → suggest `2.0.0-preview.2`
- Current `2.0.0-rc.1` → suggest `2.0.0-rc.2`
Present the suggestion and let the user confirm or provide an alternative. Present the suggestion and let the user confirm or provide an alternative.
Parse the confirmed version into its `VersionPrefix` component. Parse the confirmed version into its `VersionPrefix` and `VersionSuffix` components. Stable versions have no suffix.
### Step 3: Create Pull Request ### Step 3: Create Pull Request
1. Create a new branch named `bump-version-to-{version}` (e.g. `bump-version-to-1.1.0`) from the default branch 1. Create a new branch named `bump-version-to-{version}` (e.g. `bump-version-to-1.1.0`) from the current branch
2. Update `src/Directory.Build.props`: 2. Update `src/Directory.Build.props`:
- Set `<VersionPrefix>` to the new version - Set `<VersionPrefix>` to the confirmed stable component
- Set `<VersionSuffix>` for prerelease versions, or clear it for stable versions; add the element if it is missing
- Update `<PackageValidationBaselineVersion>` if the MAJOR version has changed - Update `<PackageValidationBaselineVersion>` if the MAJOR version has changed
3. Commit with message: `Bump version to {version}` 3. Commit with message: `Bump version to {version}`
4. Push the branch and create a pull request: 4. Push the branch and create a pull request:
- **Title**: `Bump version to {version}` - **Title**: `Bump version to {version}`
- **Label**: `infrastructure` - **Label**: `infrastructure`
- **Base**: default branch - **Base**: the current branch (which, for a servicing branch like `release/1.x`, is that servicing branch — not `main`)
### Step 4: Confirm ### Step 4: Confirm

View file

@ -56,13 +56,31 @@ Recommend a PATCH version increment if no MAJOR or MINOR criteria are met.
- MINOR: `MAJOR.(MINOR+1).0` - MINOR: `MAJOR.(MINOR+1).0`
- PATCH: `MAJOR.MINOR.(PATCH+1)` - PATCH: `MAJOR.MINOR.(PATCH+1)`
**Examples** from previous release `v1.2.0`: ### Prereleases
| Level | Recommended | While the candidate version uses a prerelease suffix (e.g., `X.Y.Z-preview.N`, `X.Y.Z-rc.N`), the recommended next version increments the trailing integer of the suffix: `preview.3` → `preview.4`, `rc.1` → `rc.2`.
|-------|-------------|
| PATCH | `v1.2.1` | Going to GA drops the suffix entirely: `2.0.0-rc.2` → `2.0.0`.
| MINOR | `v1.3.0` |
| MAJOR | `v2.0.0` | This is purely about how to *compute* the next version. It does **not** declare any new policy about what kinds of changes are permitted between previews — refer to the existing [versioning documentation](../../../../docs/versioning.html) for breaking-change policy.
### Branch context
The "previous release" lookup is constrained to tags matching `v{MAJOR}.*` when assessing from a `release/{MAJOR}.x` servicing branch. On `main`, the lookup is unconstrained (most recent published release globally).
The MAJOR/MINOR/PATCH classification criteria above are unchanged regardless of branch.
See [release-branches.md](../../shared-resources/release-branches.md) for branch-role definitions and previous-release lookup rules.
**Examples**:
| Previous release | Branch | Level | Recommended |
|--------------------|---------------|-----------------------|------------------------|
| `v1.2.0` | `main` | PATCH | `v1.2.1` |
| `v1.2.0` | `main` | MINOR | `v1.3.0` |
| `v1.2.0` | `main` | MAJOR | `v2.0.0` |
| `v2.0.0-preview.1` | `main` | (prerelease bump) | `v2.0.0-preview.2` |
| `v1.3.0` | `release/1.x` | PATCH | `v1.3.1` |
## Comparing Against the Candidate Version ## Comparing Against the Candidate Version
@ -83,6 +101,7 @@ Present the assessment as a summary table followed by a rationale:
| Aspect | Finding | | Aspect | Finding |
|--------|---------| |--------|---------|
| Branch context | release/1.x |
| Previous release | v1.0.0 | | Previous release | v1.0.0 |
| Breaking changes | None confirmed | | Breaking changes | None confirmed |
| New API surface | Yes — 3 PRs add new public APIs | | New API surface | Yes — 3 PRs add new public APIs |

View file

@ -12,22 +12,39 @@ Prepare a new release for the `modelcontextprotocol/csharp-sdk` repository. This
> **User confirmation required: This skill NEVER pushes a branch or creates a pull request without explicit user confirmation.** The user must review and approve all details before any remote operations occur. > **User confirmation required: This skill NEVER pushes a branch or creates a pull request without explicit user confirmation.** The user must review and approve all details before any remote operations occur.
Use the shared [release branch reference](../shared-resources/release-branches.md) for branch roles, previous-release lookup rules, and release work-branch naming.
## Process ## Process
Work through each step sequentially. Present findings at each step and get user confirmation before proceeding. Skip any step that has no applicable items. Work through each step sequentially. Present findings at each step and get user confirmation before proceeding. Skip any step that has no applicable items.
### Step 1: Determine Target and Gather PRs ### Step 1: Select Source Branch
List candidate source/base branches via:
`gh api repos/{owner}/{repo}/branches --paginate --jq '[.[] | select(.name == "main" or (.name | startswith("release/"))) | .name]'`
Present the list to the user and ask them to choose the source/base branch. Default selection: `main`.
The selected branch drives every subsequent step:
1. The branch on which the candidate version is read from `src/Directory.Build.props`.
2. The "previous release" lookup (constrained to `v{MAJOR}.*` on `release/{MAJOR}.x`).
3. The commit range from which PRs are collected.
4. The PR base (`--base`) for `gh pr create` at the end of the skill.
See [release-branches.md](../shared-resources/release-branches.md) for the structured branch rules.
### Step 2: Determine Target and Gather PRs
The user may provide: The user may provide:
- **A git ref** (commit SHA, branch, or tag) — use as the target commit - **A git ref** (commit SHA, branch, or tag) — use as the target commit relative to the selected source/base branch
- **No context** — show the last 5 commits on `main` (noting HEAD) and offer the option to enter a branch or tag name instead - **No context** — show the last 5 commits on the selected source/base branch (noting HEAD) and offer the option to enter a branch or tag name instead
Once the target is established: Once the target is established:
1. Determine the previous release tag from `gh release list` (most recent **published** release — exclude drafts with `--exclude-drafts`). 1. Determine the previous release tag from `gh release list` (most recent **published** release — exclude drafts with `--exclude-drafts`). Use the selected source/base branch context: on `release/{MAJOR}.x`, restrict candidates to tags matching `v{MAJOR}.*`; on `main`, use the most recent published release globally.
2. Get the full list of PRs merged between the previous release tag and the target commit. 2. Get the full list of PRs merged between the previous release tag and the target commit on the selected branch.
3. Read `src/Directory.Build.props` **at the target commit**. Extract `<VersionPrefix>` as the **candidate version**. 3. Read `src/Directory.Build.props` **at the target commit**. Extract `<VersionPrefix>` and `<VersionSuffix>`; the **candidate version** is `{VersionPrefix}` plus `-{VersionSuffix}` when the suffix is present (for example, `2.0.0-preview.1`).
### Step 2: Categorize and Attribute ### Step 3: Categorize and Attribute
Sort every PR into one of four categories. See [references/categorization.md](references/categorization.md) for detailed guidance. Sort every PR into one of four categories. See [references/categorization.md](references/categorization.md) for detailed guidance.
@ -50,53 +67,55 @@ Sort every PR into one of four categories. See [references/categorization.md](re
- Omit the co-author parenthetical when there are none - Omit the co-author parenthetical when there are none
- Sort entries within each section by merge date (chronological) - Sort entries within each section by merge date (chronological)
### Step 3: Breaking Change Audit ### Step 4: Breaking Change Audit
Invoke the **breaking-changes** skill with the commit range from the previous release tag to the target commit. Examine every PR, assess impact, reconcile labels (offering to add/remove labels and comment on PRs), and get user confirmation. Invoke the **breaking-changes** skill with the commit range from the previous release tag to the target commit. Examine every PR, assess impact, reconcile labels (offering to add/remove labels and comment on PRs), and get user confirmation.
Use the results (confirmed breaking changes with impact ordering and detail bullets) in the remaining steps. Use the results (confirmed breaking changes with impact ordering and detail bullets) in the remaining steps.
### Step 4: Assess Release Version ### Step 5: Assess Release Version
Using the categorized PRs from Step 2 and confirmed breaking changes from Step 3, assess the appropriate [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) release level. Follow the [SemVer assessment guide](../bump-version/references/semver-assessment.md) (owned by the **bump-version** skill) for the full assessment criteria. Using the categorized PRs from Step 3 and confirmed breaking changes from Step 4, assess the appropriate [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) release level. Follow the [SemVer assessment guide](../bump-version/references/semver-assessment.md) (owned by the **bump-version** skill) for the full assessment criteria.
1. **Classify the release level**: 1. **Classify the release level**:
- **MAJOR** — if any confirmed breaking changes are present (API or behavioral), excluding changes to `[Experimental]` APIs - **MAJOR** — if any confirmed breaking changes are present (API or behavioral), excluding changes to `[Experimental]` APIs
- **MINOR** — if no breaking changes but new public APIs, features, or obsoletion warnings are introduced - **MINOR** — if no breaking changes but new public APIs, features, or obsoletion warnings are introduced
- **PATCH** — otherwise - **PATCH** — otherwise
2. **Compute the recommended version** from the previous release tag: 2. **Compute the recommended version** from the previous release tag and branch context:
- Increment the appropriate component (MAJOR resets MINOR.PATCH to 0; MINOR resets PATCH to 0) - Increment the appropriate component (MAJOR resets MINOR.PATCH to 0; MINOR resets PATCH to 0)
- For prerelease candidates such as `preview.N` or `rc.N`, the recommendation may simply increment the trailing integer per the assessment guide
3. **Compare against the candidate version** from `src/Directory.Build.props`. Flag any discrepancy: 3. **Compare against the candidate version** from `src/Directory.Build.props`. Flag any discrepancy:
- **Under-versioned**: The candidate is lower than the recommended level. This is a concern that should be resolved. - **Under-versioned**: The candidate is lower than the recommended level. This is a concern that should be resolved.
- **Over-versioned**: The candidate is higher than strictly required. This is acceptable under SemVer but worth noting. - **Over-versioned**: The candidate is higher than strictly required. This is acceptable under SemVer but worth noting.
4. **Present the assessment** with a summary table showing the previous release, change classification, recommended level, recommended version, and any discrepancy with the candidate. Include a brief rationale citing the most significant PRs. 4. **Present the assessment** with a summary table showing the previous release, change classification, recommended level, recommended version, and any discrepancy with the candidate. Include a brief rationale citing the most significant PRs.
5. **Get user confirmation** of the release version before proceeding. 5. **Get user confirmation** of the release version before proceeding.
### Step 5: Create Release Branch and Bump Version ### Step 6: Create Release Branch and Bump Version
After the version is confirmed: After the version is confirmed:
1. Create a local branch named `release-{version}` from the target commit (e.g., `release-1.1.0`). 1. Create a local branch named `release-{version}` from the target commit (e.g., `release-2.0.0-preview.1`, `release-1.3.1`).
2. Update `src/Directory.Build.props`: 2. Update `src/Directory.Build.props`:
- Set `<VersionPrefix>` to the confirmed version - Set `<VersionPrefix>` to the confirmed stable component
- Update `<PackageValidationBaselineVersion>` if the MAJOR version has changed (set to the previous release version) - Set `<VersionSuffix>` for prerelease versions, or clear it for stable versions; add the element if it is missing
- Update `<PackageValidationBaselineVersion>` when appropriate. For the `2.0.0-preview` series, baseline is `1.3.0` (latest shipped 1.x). For subsequent stable releases, baseline is the previous shipped version of the same MAJOR or the latest stable from the previous MAJOR.
3. Build the solution to verify the version change compiles: `dotnet build` 3. Build the solution to verify the version change compiles: `dotnet build`
This step creates local changes only — nothing is committed or pushed yet. This step creates local changes only — nothing is committed or pushed yet.
### Step 6: Run API Compatibility Check ### Step 7: Run API Compatibility Check
Run API compatibility validation against the baseline version. Follow [references/apicompat-apidiff.md](references/apicompat-apidiff.md) for the full procedure. Run API compatibility validation against the baseline version. Follow [references/apicompat-apidiff.md](references/apicompat-apidiff.md) for the full procedure.
1. Run `dotnet pack` to trigger package validation against `PackageValidationBaselineVersion` 1. Run `dotnet pack` to trigger package validation against `PackageValidationBaselineVersion`
2. Capture the ApiCompat output (compatibility issues, warnings, suppressions) 2. Capture the ApiCompat output (compatibility issues, warnings, suppressions)
3. If there are unexpected compatibility breaks: 3. If there are unexpected compatibility breaks:
- Cross-reference with the breaking change audit from Step 3 - Cross-reference with the breaking change audit from Step 4
- Present any unaccounted breaks to the user - Present any unaccounted breaks to the user
- If breaks are intentional, add appropriate entries to `CompatibilitySuppressions.xml` in the affected project directory - If breaks are intentional, add appropriate entries to `CompatibilitySuppressions.xml` in the affected project directory
4. Record the ApiCompat results for inclusion in the PR description 4. Record the ApiCompat results for inclusion in the PR description
### Step 7: Generate API Diff Report ### Step 8: Generate API Diff Report
Generate a human-readable diff of the public API surface between the previous release and the new version. Follow [references/apicompat-apidiff.md](references/apicompat-apidiff.md) for the full procedure, including how to install the `Microsoft.DotNet.ApiDiff.Tool` from the .NET transport feed. Generate a human-readable diff of the public API surface between the previous release and the new version. Follow [references/apicompat-apidiff.md](references/apicompat-apidiff.md) for the full procedure, including how to install the `Microsoft.DotNet.ApiDiff.Tool` from the .NET transport feed.
@ -107,7 +126,7 @@ Generate a human-readable diff of the public API surface between the previous re
> **If the ApiDiff tool cannot be installed or fails to produce output, STOP and inform the user.** Present the error and ask how to proceed. Do not fall back to a manual summary — the user must decide whether to troubleshoot, skip the API diff, or abort. > **If the ApiDiff tool cannot be installed or fails to produce output, STOP and inform the user.** Present the error and ask how to proceed. Do not fall back to a manual summary — the user must decide whether to troubleshoot, skip the API diff, or abort.
### Step 8: Review and Update Documentation ### Step 9: Review and Update Documentation
Review repository documentation for changes needed to compensate for or adapt to this release: Review repository documentation for changes needed to compensate for or adapt to this release:
@ -118,12 +137,12 @@ Review repository documentation for changes needed to compensate for or adapt to
Stage all documentation changes for inclusion in the release commit. Stage all documentation changes for inclusion in the release commit.
### Step 9: Draft Release Notes ### Step 10: Draft Release Notes
Compose the release notes that will appear in the PR description and serve as the foundation for the **publish-release** skill. This is a draft — the final release notes will be refreshed when the GitHub release is created. Compose the release notes that will appear in the PR description and serve as the foundation for the **publish-release** skill. This is a draft — the final release notes will be refreshed when the GitHub release is created.
1. **Preamble** — Draft a short paragraph summarizing the release theme. Present it to the user for review and editing. The preamble is **required**. 1. **Preamble** — Draft a short paragraph summarizing the release theme. Present it to the user for review and editing. The preamble is **required**.
2. **Breaking Changes** — sorted most → least impactful (from Step 3 results). Include the versioning docs link. 2. **Breaking Changes** — sorted most → least impactful (from Step 4 results). Include the versioning docs link.
3. **What's Changed** — chronological; includes breaking change PRs 3. **What's Changed** — chronological; includes breaking change PRs
4. **Documentation Updates** — chronological 4. **Documentation Updates** — chronological
5. **Test Improvements** — chronological 5. **Test Improvements** — chronological
@ -132,11 +151,11 @@ Compose the release notes that will appear in the PR description and serve as th
- New contributors (first contribution in this release) - New contributors (first contribution in this release)
- Issue reporters (cite resolving PRs) - Issue reporters (cite resolving PRs)
- PR reviewers (single bullet, sorted by review count, no count shown) - PR reviewers (single bullet, sorted by review count, no count shown)
8. **Full Changelog** link 8. **Full Changelog** link using the exact tag, including any suffix (for example, `v1.3.1` or `v2.0.0-preview.1`)
Omit empty sections. Present each section for user review before proceeding. Omit empty sections. Present each section for user review before proceeding. Tag references in templates use `v{version}` exactly, including prerelease suffixes; the Full Changelog link compares the previous tag to the suffixed tag when applicable.
### Step 10: Commit Changes ### Step 11: Commit Changes
Commit all changes to the `release-{version}` branch: Commit all changes to the `release-{version}` branch:
@ -144,39 +163,40 @@ Commit all changes to the `release-{version}` branch:
2. Commit with message: `Prepare release v{version}` 2. Commit with message: `Prepare release v{version}`
3. Do **not** push yet 3. Do **not** push yet
### Step 11: Present Release Summary ### Step 12: Present Release Summary
Present **all** of the following details to the user for review. The user must confirm every aspect before proceeding to Step 12. Present **all** of the following details to the user for review. The user must confirm every aspect before proceeding to Step 13.
1. **Version number** with brief rationale for why this SemVer level was selected 1. **Version number** with brief rationale for why this SemVer level was selected
2. **Branch name** (e.g., `release-1.1.0`) 2. **Source/base branch** selected in Step 1
3. **Remote** the branch would be pushed to (show the configured remote, typically `origin`) 3. **Branch name** (e.g., `release-2.0.0-preview.1`, `release-1.3.1`)
4. **Files changed** — list every file modified in the commit with a one-line summary of what changed in each: 4. **Remote** the branch would be pushed to (show the configured remote, typically `origin`)
5. **Files changed** — list every file modified in the commit with a one-line summary of what changed in each:
``` ```
src/Directory.Build.props — Version bumped from 1.0.0 to 1.1.0 src/Directory.Build.props — Version bumped from 2.0.0-preview.1 to 2.0.0-preview.2
src/ModelContextProtocol.Core/CompatibilitySuppressions.xml — Added 2 new suppressions src/ModelContextProtocol.Core/CompatibilitySuppressions.xml — Added 2 new suppressions
README.md — Updated code sample for new API README.md — Updated code sample for new API
docs/experimental.md — Added new experimental API reference docs/experimental.md — Added new experimental API reference
``` ```
5. **Draft release notes** — the complete release notes from Step 9 6. **Draft release notes** — the complete release notes from Step 10
6. **API Compatibility results** — the ApiCompat output from Step 6 7. **API Compatibility results** — the ApiCompat output from Step 7
7. **API Diff report** — the API diff from Step 7 8. **API Diff report** — the API diff from Step 8
8. **Proposed PR title** (e.g., `Release v1.1.0`) 9. **Proposed PR title** (e.g., `Release v2.0.0-preview.1`, `Release v1.3.1`)
9. **Proposed PR description** — the assembled content combining release notes, ApiCompat, and ApiDiff 10. **Proposed PR description** — the assembled content combining release notes, ApiCompat, and ApiDiff
After presenting all details, explicitly ask the user: After presenting all details, explicitly ask the user:
> Would you like to push the branch and create the pull request? > Would you like to push the branch and create the pull request?
**Do not proceed without explicit "yes" confirmation.** **Do not proceed without explicit "yes" confirmation.**
### Step 12: Push Branch and Create Pull Request ### Step 13: Push Branch and Create Pull Request
Only after explicit user confirmation in Step 11: Only after explicit user confirmation in Step 12:
1. Push the `release-{version}` branch to the remote 1. Push the `release-{version}` branch to the remote
2. Create a pull request: 2. Create a pull request with `gh pr create --base {step-1-branch}`:
- **Title**: `Release v{version}` - **Title**: `Release v{version}`
- **Base**: default branch (typically `main`) - **Base**: the source/base branch selected in Step 1
- **Head**: `release-{version}` - **Head**: `release-{version}`
- **Description**: The assembled PR description (see PR Description Template below) - **Description**: The assembled PR description (see PR Description Template below)
- **Labels**: Apply appropriate labels (e.g., `release`) - **Labels**: Apply appropriate labels (e.g., `release`)
@ -192,18 +212,20 @@ Only after explicit user confirmation in Step 11:
- **Single breaking change**: use the same numbered format as multiple - **Single breaking change**: use the same numbered format as multiple
- **No user-facing changes**: if all PRs are documentation, tests, or infrastructure, flag that a release may not be warranted and ask the user whether to proceed - **No user-facing changes**: if all PRs are documentation, tests, or infrastructure, flag that a release may not be warranted and ask the user whether to proceed
- **Version discrepancy**: if the candidate version from `Directory.Build.props` doesn't match the SemVer assessment, present the discrepancy and let the user decide the final version - **Version discrepancy**: if the candidate version from `Directory.Build.props` doesn't match the SemVer assessment, present the discrepancy and let the user decide the final version
- **Proposed MAJOR does not match branch MAJOR**: if the proposed version's MAJOR doesn't match the branch's MAJOR (for example, proposing `2.0.0-preview.2` on `release/1.x`), flag this as a warning and ask the user to confirm. Do not hard-fail. This is informational, not a policy enforcement.
- **Prerelease bump**: when the candidate version has a suffix like `preview.N`, the SemVer assessment may simply increment `N` rather than computing MAJOR/MINOR/PATCH. Refer to the SemVer assessment guide's Prereleases section.
- **No previous release**: if this is the first release, there is no previous tag; gather all PRs merged to the target - **No previous release**: if this is the first release, there is no previous tag; gather all PRs merged to the target
- **ApiCompat tooling unavailable**: fall back to `dotnet pack` output; note in the PR description that full ApiCompat was run via package validation only - **ApiCompat tooling unavailable**: fall back to `dotnet pack` output; note in the PR description that full ApiCompat was run via package validation only
- **API diff tool installation fails**: do not fall back to a manual summary; pause and present the installation error to the user, offering options to troubleshoot, skip the API diff section, or abort the release preparation - **API diff tool installation fails**: do not fall back to a manual summary; pause and present the installation error to the user, offering options to troubleshoot, skip the API diff section, or abort the release preparation
- **No changelogs in repo**: skip changelog updates; note in the summary - **No changelogs in repo**: skip changelog updates; note in the summary
- **Branch already exists**: if `release-{version}` already exists locally or remotely, ask the user whether to reuse it, delete and recreate, or choose a different name - **Branch already exists**: if `release-{version}` already exists locally or remotely, ask the user whether to reuse it, delete and recreate, or choose a different name
- **PackageValidationBaselineVersion update**: when bumping MAJOR version, update the baseline to the previous release version; when bumping MINOR or PATCH, keep the existing baseline - **PackageValidationBaselineVersion update**: for the `2.0.0-preview` series, use `1.3.0`; for subsequent stable releases, use the previous shipped version of the same MAJOR or the latest stable from the previous MAJOR
- **CompatibilitySuppressions.xml**: when intentional breaks are found, add suppression entries and include the file in the commit; existing suppressions should be preserved - **CompatibilitySuppressions.xml**: when intentional breaks are found, add suppression entries and include the file in the commit; existing suppressions should be preserved
- **User declines PR creation**: if the user declines at Step 11, leave the local branch intact so they can review, modify, or push manually - **User declines PR creation**: if the user declines at Step 12, leave the local branch intact so they can review, modify, or push manually
## PR Description Template ## PR Description Template
The PR description combines release notes, ApiCompat, and ApiDiff into a single document. Omit empty sections. The PR description combines release notes, ApiCompat, and ApiDiff into a single document. Omit empty sections. The `{version}` placeholder is the full version and may include a prerelease suffix (for example, `Release v2.0.0-preview.1`).
```markdown ```markdown
# Release v{version} # Release v{version}
@ -241,7 +263,8 @@ Refer to the [C# SDK Versioning](https://csharp.sdk.modelcontextprotocol.io/vers
* @user made their first contribution in #PR * @user made their first contribution in #PR
* @user1 @user2 @user3 reviewed pull requests * @user1 @user2 @user3 reviewed pull requests
**Full Changelog**: https://github.com/modelcontextprotocol/csharp-sdk/compare/previous-tag...release-{version} **Full Changelog**: https://github.com/modelcontextprotocol/csharp-sdk/compare/{previous-tag}...v{version}
<!-- Example: https://github.com/modelcontextprotocol/csharp-sdk/compare/v1.3.0...v2.0.0-preview.1 -->
--- ---
@ -266,7 +289,7 @@ Refer to the [C# SDK Versioning](https://csharp.sdk.modelcontextprotocol.io/vers
## Release Notes Template ## Release Notes Template
The release notes section within the PR description uses the same format as the final GitHub release notes (used by the **publish-release** skill). This ensures consistency between the PR and the published release. The release notes section within the PR description uses the same format as the final GitHub release notes (used by the **publish-release** skill). This ensures consistency between the PR and the published release. Tag examples such as `v2.0.0-preview.1` are valid and should be used verbatim when the version has a prerelease suffix.
Omit empty sections. The preamble is **always required** — it is not inside a section heading. Omit empty sections. The preamble is **always required** — it is not inside a section heading.
@ -303,5 +326,6 @@ Refer to the [C# SDK Versioning](https://csharp.sdk.modelcontextprotocol.io/vers
* @user submitted issue #1234 (resolved by #5678) * @user submitted issue #1234 (resolved by #5678)
* @user1 @user2 @user3 reviewed pull requests * @user1 @user2 @user3 reviewed pull requests
**Full Changelog**: https://github.com/modelcontextprotocol/csharp-sdk/compare/previous-tag...new-tag **Full Changelog**: https://github.com/modelcontextprotocol/csharp-sdk/compare/{previous-tag}...v{version}
<!-- Example: https://github.com/modelcontextprotocol/csharp-sdk/compare/v1.3.0...v2.0.0-preview.1 -->
``` ```

View file

@ -8,6 +8,8 @@ compatibility: Requires gh CLI with repo access and GitHub API access for PR det
Create a GitHub release for the `modelcontextprotocol/csharp-sdk` repository after a **prepare-release** PR has been merged. This skill refreshes the release notes to include any PRs merged between the preparation branch point and the merge, warns about changes that affect the version or breaking change assessment, and creates a **draft** GitHub release. Create a GitHub release for the `modelcontextprotocol/csharp-sdk` repository after a **prepare-release** PR has been merged. This skill refreshes the release notes to include any PRs merged between the preparation branch point and the merge, warns about changes that affect the version or breaking change assessment, and creates a **draft** GitHub release.
Use the shared [release branch reference](../shared-resources/release-branches.md) for branch roles, previous-release lookup rules, and release work-branch naming.
> **Safety: This skill only creates and updates draft releases. It must never publish a release.** If the user asks to publish, decline and instruct them to publish manually through the GitHub UI. > **Safety: This skill only creates and updates draft releases. It must never publish a release.** If the user asks to publish, decline and instruct them to publish manually through the GitHub UI.
## Process ## Process
@ -18,7 +20,7 @@ Work through each step sequentially. Present findings at each step and get user
The user may provide: The user may provide:
- **A PR number or URL** — use directly - **A PR number or URL** — use directly
- **A version number** (e.g., `1.1.0`) — search for a merged PR titled `Release v{version}` - **A version number** (e.g., `1.1.0`, `2.0.0-preview.1`) — search for a merged PR titled `Release v{version}`. Prerelease versions are used verbatim, for example `Release v2.0.0-preview.1`
- **No context** — list recently merged PRs with `Release v` in the title and ask the user to select - **No context** — list recently merged PRs with `Release v` in the title and ask the user to select
Verify the PR is merged. Extract: Verify the PR is merged. Extract:
@ -28,13 +30,13 @@ Verify the PR is merged. Extract:
### Step 2: Determine Version and Commit Range ### Step 2: Determine Version and Commit Range
1. Read `src/Directory.Build.props` at the merge commit to confirm `<VersionPrefix>`. The tag is `v{VersionPrefix}`. 1. Read `src/Directory.Build.props` at the merge commit to confirm `<VersionPrefix>` and `<VersionSuffix>`. The tag is `v{VersionPrefix}` plus `-{VersionSuffix}` when the suffix is present; for example, `<VersionPrefix>2.0.0</VersionPrefix>` + `<VersionSuffix>preview.1</VersionSuffix>` → `v2.0.0-preview.1`.
2. Determine the previous release tag from `gh release list` (most recent **published** release — exclude drafts with `--exclude-drafts`). 2. Determine the previous release tag from `gh release list` (most recent **published** release — exclude drafts with `--exclude-drafts`). The lookup is branch-aware: when the merge commit is on a `release/{MAJOR}.x` branch, restrict candidates to tags matching `v{MAJOR}.*`; on `main`, use the most recent published release globally. See [release-branches.md](../shared-resources/release-branches.md).
3. Identify the full commit range: previous release tag → merge commit. 3. Identify the full commit range: previous release tag → merge commit.
### Step 3: Check for Additional PRs ### Step 3: Check for Additional PRs
Compare the PRs included in the original prepare-release PR description with the full set of PRs now merged in the commit range. Use the [SemVer assessment guide](../bump-version/references/semver-assessment.md) (owned by the **bump-version** skill) to evaluate the impact of any new PRs against the version that was committed during preparation. Compare the PRs included in the original prepare-release PR description with the full set of PRs now merged in the commit range. Use the [SemVer assessment guide](../bump-version/references/semver-assessment.md) (owned by the **bump-version** skill) to evaluate the impact of any new PRs against the version that was committed during preparation, including its prerelease and branch-context computation rules. This is not a policy change; only the version computation and previous-release lookup change.
1. Extract the PR list from the prepare-release PR description (all `#NNN` references in release notes sections). 1. Extract the PR list from the prepare-release PR description (all `#NNN` references in release notes sections).
2. Get the full set of PRs merged between the previous release tag and the merge commit. 2. Get the full set of PRs merged between the previous release tag and the merge commit.
@ -107,11 +109,11 @@ Follow [references/formatting.md](references/formatting.md) when composing and u
### Step 9: Create Draft Release ### Step 9: Create Draft Release
Display release metadata for user review: Display release metadata for user review:
- **Title / Tag**: the confirmed version (e.g. `v1.1.0`) - **Title / Tag**: the confirmed tag, including any prerelease suffix (e.g. `v1.3.1`, `v2.0.0-preview.1`)
- **Target**: merge commit SHA, its message, and the prepare-release PR link - **Target**: merge commit SHA, its message, the merge commit's branch (the prepare-release PR base), and the prepare-release PR link
After confirmation: After confirmation:
- Create with `gh release create --draft` (always `--draft`) - Create with `gh release create --draft {tag} --target {merge-commit-branch}` (always `--draft`), using the prerelease tag verbatim when present
- **Never publish.** If the user asks to publish, decline and instruct them to publish manually. - **Never publish.** If the user asks to publish, decline and instruct them to publish manually.
When the user requests revisions after the initial creation, always rewrite the complete body as a file — never perform in-place string replacements. See [references/formatting.md](references/formatting.md). When the user requests revisions after the initial creation, always rewrite the complete body as a file — never perform in-place string replacements. See [references/formatting.md](references/formatting.md).
@ -131,7 +133,7 @@ When the user requests revisions after the initial creation, always rewrite the
## Release Notes Template ## Release Notes Template
Omit empty sections. The preamble is **always required** — it is not inside a section heading. Omit empty sections. The preamble is **always required** — it is not inside a section heading. Tags may include prerelease suffixes, such as `v2.0.0-preview.1`, and Full Changelog compare links should use the exact tag.
```markdown ```markdown
[Preamble — REQUIRED. Summarize the release theme.] [Preamble — REQUIRED. Summarize the release theme.]
@ -166,5 +168,6 @@ Refer to the [C# SDK Versioning](https://csharp.sdk.modelcontextprotocol.io/vers
* @user submitted issue #1234 (resolved by #5678) * @user submitted issue #1234 (resolved by #5678)
* @user1 @user2 @user3 reviewed pull requests * @user1 @user2 @user3 reviewed pull requests
**Full Changelog**: https://github.com/modelcontextprotocol/csharp-sdk/compare/previous-tag...new-tag **Full Changelog**: https://github.com/modelcontextprotocol/csharp-sdk/compare/{previous-tag}...v{version}
<!-- Example: https://github.com/modelcontextprotocol/csharp-sdk/compare/v1.3.0...v2.0.0-preview.1 -->
``` ```

View file

@ -0,0 +1,40 @@
# Release Branches
Shared reference for release skills. Describes the branch roles used by the release workflow and the rules each skill follows for selecting a branch and looking up the previous release.
## Branch roles
| Branch | Purpose | CI behavior |
| ------------------- | ----------------------------------------------- | -------------------------------------- |
| `main` | Next-MAJOR preview/development line | Nightly `cron` build → GitHub Packages |
| `release/{MAJOR}.x` | Long-lived servicing branch for a shipped MAJOR | Every push → GitHub Packages |
| `release-{version}` | Short-lived release preparation branch | Built by PR CI; no package publishing |
Official NuGet.org publishes happen only when a GitHub Release is created from a branch's tag.
## Selecting a source/base branch (`prepare-release` Step 1)
1. List candidate branches via:
`gh api repos/{owner}/{repo}/branches --paginate --jq '[.[] | select(.name == "main" or (.name | startswith("release/"))) | .name]'`
2. Present the list to the user. Default selection: `main`.
3. The selected branch drives:
- Previous-release lookup (see below).
- The branch on which the candidate version is read from `src/Directory.Build.props`.
- The commit range from which PRs are collected.
- The `--base` of the PR created at the end of the skill.
## Previous-release tag lookup
- On `main`: most recent published release globally (use `gh release list --exclude-drafts --limit 50` and pick the highest semver). No MAJOR filter.
- On `release/{MAJOR}.x`: most recent published release whose tag matches `v{MAJOR}.*`. Drafts are excluded.
This is purely a baseline-selection rule. It does **not** change the breaking-change policy. See [the versioning docs](https://csharp.sdk.modelcontextprotocol.io/versioning.html) for the policy.
## Work-branch naming
Prepare-release work branches are named `release-{version}` (flat, hyphen-separated):
- `release-2.0.0-preview.1`
- `release-1.3.1`
- `release-2.0.0`
Hyphens in prerelease versions are valid in git branch names.

View file

@ -24,7 +24,7 @@ jobs:
pattern: testresults-* pattern: testresults-*
- name: Combine coverage reports - name: Combine coverage reports
uses: danielpalme/ReportGenerator-GitHub-Action@5.5.5 uses: danielpalme/ReportGenerator-GitHub-Action@7ae927204961589fcb0b0be245c51fbbc87cbca2 # 5.5.5
with: with:
reports: "**/*.cobertura.xml" reports: "**/*.cobertura.xml"
targetdir: "${{ github.workspace }}/report" targetdir: "${{ github.workspace }}/report"

View file

@ -1,19 +1,24 @@
# Publish new package versions of ModelContextProtocol # Publish new package versions of ModelContextProtocol
# #
# Daily and Manual Runs # Triggers
# - Triggered automatically at 07:00 UTC daily # - Scheduled (07:00 UTC daily, main branch): produces a CI-suffixed package
# - Triggered manually using GitHub Actions workflow_dispatch event # and publishes it to the GitHub package registry.
# - Version prefix applied from /src/Directory.Build.props
# - Version suffix set to `ci.{github.run_number}`
# - Package published to GitHub package registry
# #
# Official Releases # - Push to `release/**`: every commit to a release branch produces a CI-suffixed
# - Triggered after a GitHub Release is created # package and publishes it to the GitHub package registry.
# - Version prefix applied from /src/Directory.Build.props #
# - Version suffix applied from /src/Directory.Build.props # - Manual `workflow_dispatch` (any branch): same outputs as a scheduled/push build;
# - Package published to GitHub package registry # accepts a `version_suffix_override` input.
# - Package published to NuGet.org #
# - Version prefix and/or suffix should be updated after each release # - GitHub Release published (any branch's tag): publishes the version from
# `src/Directory.Build.props` to the GitHub package registry AND NuGet.org.
#
# Version
# - Version prefix and suffix come from `src/Directory.Build.props`.
# - For non-release triggers, the suffix is replaced with `ci.{run_number}`
# (or `workflow_dispatch.inputs.version_suffix_override` when provided).
# - The prefix and suffix in `Directory.Build.props` should be updated after each
# release using the `bump-version` or `prepare-release` skills.
name: Release Publishing name: Release Publishing
@ -21,6 +26,10 @@ on:
schedule: schedule:
- cron: '0 7 * * *' - cron: '0 7 * * *'
push:
branches:
- 'release/**'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version_suffix_override: version_suffix_override:

View file

@ -1,11 +1,12 @@
<Solution> <Solution>
<Folder Name="/.github/" /> <Folder Name="/.github/">
<File Path=".github/release-process.md" />
</Folder>
<Folder Name="/.github/workflows/"> <Folder Name="/.github/workflows/">
<File Path=".github/workflows/ci-build-test.yml" /> <File Path=".github/workflows/ci-build-test.yml" />
<File Path=".github/workflows/ci-code-coverage.yml" /> <File Path=".github/workflows/ci-code-coverage.yml" />
<File Path=".github/workflows/docs.yml" /> <File Path=".github/workflows/docs.yml" />
<File Path=".github/workflows/markdown-link-check.yml" /> <File Path=".github/workflows/markdown-link-check.yml" />
<File Path=".github/workflows/release.md" />
<File Path=".github/workflows/release.yml" /> <File Path=".github/workflows/release.yml" />
</Folder> </Folder>
<Folder Name="/docs/" /> <Folder Name="/docs/" />

View file

@ -31,6 +31,11 @@ For more information about MCP:
- [Protocol Specification](https://modelcontextprotocol.io/specification/) - [Protocol Specification](https://modelcontextprotocol.io/specification/)
- [GitHub Organization](https://github.com/modelcontextprotocol) - [GitHub Organization](https://github.com/modelcontextprotocol)
## Cross-Application Access (Identity Assertion Authorization Grant flow)
The SDK provides support for the [Identity Assertion Authorization Grant flow](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx)
via `IdentityAssertionGrantProvider`. See the [Cross-Application Access](docs/concepts/transports/transports.md#cross-application-access) section in the transport docs for full usage details.
## License ## License
This project is licensed under the [Apache License 2.0](LICENSE). This project is licensed under the [Apache License 2.0](LICENSE).

View file

@ -13,7 +13,7 @@ uid: tasks
The Model Context Protocol (MCP) supports [task-based execution] for long-running operations. Tasks enable a "call-now, fetch-later" pattern where clients can initiate operations that may take significant time to complete, then poll for status and retrieve results when ready. The Model Context Protocol (MCP) supports [task-based execution] for long-running operations. Tasks enable a "call-now, fetch-later" pattern where clients can initiate operations that may take significant time to complete, then poll for status and retrieve results when ready.
[task-based execution]: https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks [task-based execution]: https://modelcontextprotocol.io/seps/1686-tasks
## Overview ## Overview
@ -601,4 +601,4 @@ While this file-based approach demonstrates the pattern, production systems shou
- <xref:ModelContextProtocol.InMemoryMcpTaskStore> - <xref:ModelContextProtocol.InMemoryMcpTaskStore>
- <xref:ModelContextProtocol.Protocol.McpTask> - <xref:ModelContextProtocol.Protocol.McpTask>
- <xref:ModelContextProtocol.Protocol.McpTaskStatus> - <xref:ModelContextProtocol.Protocol.McpTaskStatus>
- [MCP Tasks Specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks) - [MCP Tasks Specification](https://modelcontextprotocol.io/seps/1686-tasks)

View file

@ -39,11 +39,63 @@ Key <xref:ModelContextProtocol.Client.StdioClientTransportOptions> properties:
| `Command` | The executable to launch (required) | | `Command` | The executable to launch (required) |
| `Arguments` | Command-line arguments for the process | | `Arguments` | Command-line arguments for the process |
| `WorkingDirectory` | Working directory for the server process | | `WorkingDirectory` | Working directory for the server process |
| `EnvironmentVariables` | Environment variables (merged with current; `null` values remove variables) | | `EnvironmentVariables` | Environment variables (merged with current when inheriting; `null` values remove variables) |
| `InheritEnvironmentVariables` | Whether the server process inherits the current process's environment variables (default: `true`) |
| `ShutdownTimeout` | Graceful shutdown timeout (default: 5 seconds) | | `ShutdownTimeout` | Graceful shutdown timeout (default: 5 seconds) |
| `StandardErrorLines` | Callback for stderr output from the server process | | `StandardErrorLines` | Callback for stderr output from the server process |
| `Name` | Optional transport identifier for logging | | `Name` | Optional transport identifier for logging |
#### Environment variable inheritance
By default, the server process inherits **all** environment variables from the current process. This includes credentials, tokens, proxy settings, and internal configuration that may be sensitive or irrelevant to the server. When running third-party or untrusted MCP servers, consider disabling inheritance to prevent unintentional credential leakage:
```csharp
var transport = new StdioClientTransport(new StdioClientTransportOptions
{
Command = "my-mcp-server",
InheritEnvironmentVariables = false,
EnvironmentVariables = StdioClientTransportOptions.GetDefaultEnvironmentVariables(),
});
```
`GetDefaultEnvironmentVariables()` returns a curated set of environment variables (such as `PATH`, `HOME`, and standard system directories) that most child processes need to start correctly, without leaking credentials or other sensitive values from the parent process. The allowlist is aligned with the defaults used by the TypeScript and Python MCP SDKs. On Windows it also includes `PATHEXT`, which is required for the OS to recognize `.cmd` and `.bat` files as executable. You can add server-specific variables on top:
```csharp
var env = StdioClientTransportOptions.GetDefaultEnvironmentVariables();
env["MY_SERVER_API_KEY"] = apiKey;
var transport = new StdioClientTransport(new StdioClientTransportOptions
{
Command = "my-mcp-server",
InheritEnvironmentVariables = false,
EnvironmentVariables = env,
});
```
If you need to selectively forward a specific set of variables from the parent environment rather than using the curated allowlist, build the dictionary manually:
```csharp
var env = new Dictionary<string, string?>();
foreach (var name in new[] { "PATH", "HOME", "HTTP_PROXY", "HTTPS_PROXY" })
{
var value = Environment.GetEnvironmentVariable(name);
if (value is not null)
env[name] = value;
}
var transport = new StdioClientTransport(new StdioClientTransportOptions
{
Command = "my-mcp-server",
InheritEnvironmentVariables = false,
EnvironmentVariables = env,
});
```
> [!WARNING]
> **Security risk (inheriting):** Variables such as `AWS_SECRET_ACCESS_KEY`, `GITHUB_TOKEN`, `OPENAI_API_KEY`, and similar credentials present in the parent process automatically flow into the child process unless inheritance is disabled. This can unintentionally expose sensitive values to third-party or untrusted MCP servers.
>
> **Compatibility risk (not inheriting):** Disabling inheritance can cause the child process to fail to start or behave incorrectly if it relies on variables provided by the OS or shell. `GetDefaultEnvironmentVariables()` covers the most common requirements — `PATH`, `HOME`, and standard system directories — so for most servers it is a safe starting point. For servers that need additional variables not in the default set (such as `DOTNET_ROOT`, `LD_LIBRARY_PATH`, `JAVA_HOME`, or proxy settings like `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`), add them on top as shown in the example above.
#### stdio server #### stdio server
Use <xref:ModelContextProtocol.Server.StdioServerTransport> for servers that communicate over stdin/stdout: Use <xref:ModelContextProtocol.Server.StdioServerTransport> for servers that communicate over stdin/stdout:
@ -326,3 +378,42 @@ Console.WriteLine(await echo.InvokeAsync(new() { ["arg"] = "Hello World" }));
``` ```
Like [stdio](#stdio-transport), the in-memory transport is inherently single-session — there is no `Mcp-Session-Id` header, and server-to-client requests (sampling, elicitation, roots) work naturally over the bidirectional pipe. This makes it ideal for testing servers that depend on these features. See [Sessions](xref:stateless) for how session behavior varies across transports. Like [stdio](#stdio-transport), the in-memory transport is inherently single-session — there is no `Mcp-Session-Id` header, and server-to-client requests (sampling, elicitation, roots) work naturally over the bidirectional pipe. This makes it ideal for testing servers that depend on these features. See [Sessions](xref:stateless) for how session behavior varies across transports.
## Cross-Application Access
The SDK provides built-in support for the [Identity Assertion Authorization Grant (ID-JAG) flow](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx) via `IdentityAssertionGrantProvider`. This enables non-interactive enterprise SSO scenarios where users authenticate once via their enterprise Identity Provider (IdP) and access MCP servers without per-server authorization prompts.
The flow consists of two steps:
1. **RFC 8693 Token Exchange** at the enterprise IdP: OIDC ID token → JWT Authorization Grant (JAG)
2. **RFC 7523 JWT Bearer Grant** at the MCP authorization server: JAG → access token
### Usage
```csharp
using ModelContextProtocol.Authentication;
// The caller owns the HttpClient lifetime.
var httpClient = new HttpClient();
var provider = new IdentityAssertionGrantProvider(
new IdentityAssertionGrantProviderOptions
{
ClientId = "mcp-client-id",
IdpTokenEndpoint = "https://company.okta.com/oauth2/token",
IdpClientId = "idp-client-id",
IdTokenCallback = (context, cancellationToken) =>
// Fetch a fresh ID token from your SSO session.
mySsoClient.GetIdTokenAsync(cancellationToken)
},
httpClient);
var tokens = await provider.GetAccessTokenAsync(
resourceUrl: new Uri("https://mcp-server.example.com"),
authorizationServerUrl: new Uri("https://auth.mcp-server.example.com"),
cancellationToken: ct);
// Use tokens.AccessToken to authenticate against the MCP server.
// Call provider.InvalidateCache() to force a fresh token exchange on the next call.
```
The provider caches the resulting access token and reuses it until it expires. To force re-authentication (e.g. after a 401 response), call `provider.InvalidateCache()` before retrying.

View file

@ -23,7 +23,7 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T
| Diagnostic ID | Description | | Diagnostic ID | Description |
| :------------ | :---------- | | :------------ | :---------- |
| `MCPEXP001` | Experimental APIs for features in the MCP specification itself, including Tasks and Extensions. Tasks provide a mechanism for asynchronous long-running operations that can be polled for status and results (see [MCP Tasks specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks)). Extensions provide a framework for extending the Model Context Protocol while maintaining interoperability (see [SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)). | | `MCPEXP001` | Experimental APIs for features in the MCP specification itself, including Tasks and Extensions. Tasks provide a mechanism for asynchronous long-running operations that can be polled for status and results (see [MCP Tasks specification](https://modelcontextprotocol.io/seps/1686-tasks)). Extensions provide a framework for extending the Model Context Protocol while maintaining interoperability (see [SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)). |
| `MCPEXP002` | Experimental SDK APIs unrelated to the MCP specification itself, including subclassing `McpClient`/`McpServer` (see [#1363](https://github.com/modelcontextprotocol/csharp-sdk/pull/1363)) and `RunSessionHandler`, which may be removed or change signatures in a future release (consider using `ConfigureSessionOptions` instead). | | `MCPEXP002` | Experimental SDK APIs unrelated to the MCP specification itself, including subclassing `McpClient`/`McpServer` (see [#1363](https://github.com/modelcontextprotocol/csharp-sdk/pull/1363)) and `RunSessionHandler`, which may be removed or change signatures in a future release (consider using `ConfigureSessionOptions` instead). |
## Obsolete APIs ## Obsolete APIs

View file

@ -39,6 +39,8 @@ var mcpClient = await McpClient.CreateAsync(
Command = "npx", Command = "npx",
Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-everything"], Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-everything"],
Name = "Everything", Name = "Everything",
InheritEnvironmentVariables = false,
EnvironmentVariables = StdioClientTransportOptions.GetDefaultEnvironmentVariables(),
}), }),
clientOptions: new() clientOptions: new()
{ {
@ -82,4 +84,4 @@ while (true)
Console.WriteLine(); Console.WriteLine();
messages.AddMessages(updates); messages.AddMessages(updates);
} }

View file

@ -31,6 +31,8 @@ else
Name = "Demo Server", Name = "Demo Server",
Command = command, Command = command,
Arguments = arguments, Arguments = arguments,
InheritEnvironmentVariables = false,
EnvironmentVariables = GetMinimalDotNetEnvironment(),
}); });
} }
await using var mcpClient = await McpClient.CreateAsync(clientTransport!); await using var mcpClient = await McpClient.CreateAsync(clientTransport!);
@ -122,3 +124,21 @@ static string GetCurrentSourceDirectory([CallerFilePath] string? currentFile = n
Debug.Assert(!string.IsNullOrWhiteSpace(currentFile)); Debug.Assert(!string.IsNullOrWhiteSpace(currentFile));
return Path.GetDirectoryName(currentFile) ?? throw new InvalidOperationException("Unable to determine source directory."); return Path.GetDirectoryName(currentFile) ?? throw new InvalidOperationException("Unable to determine source directory.");
} }
// Returns the safe default environment variables plus extras needed by 'dotnet run'.
// Omitting variables the server doesn't need prevents unintentional leakage of
// credentials or other sensitive values present in the parent process.
static Dictionary<string, string?> GetMinimalDotNetEnvironment()
{
var env = StdioClientTransportOptions.GetDefaultEnvironmentVariables();
// 'dotnet run' also needs DOTNET_ROOT and NUGET_PACKAGES to find the .NET runtime and package cache.
foreach (var key in (string[])["DOTNET_ROOT", "NUGET_PACKAGES"])
{
var value = Environment.GetEnvironmentVariable(key);
if (value is not null)
{
env[key] = value;
}
}
return env;
}

View file

@ -5,7 +5,7 @@
<PackageProjectUrl>https://csharp.sdk.modelcontextprotocol.io</PackageProjectUrl> <PackageProjectUrl>https://csharp.sdk.modelcontextprotocol.io</PackageProjectUrl>
<RepositoryUrl>https://github.com/modelcontextprotocol/csharp-sdk</RepositoryUrl> <RepositoryUrl>https://github.com/modelcontextprotocol/csharp-sdk</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<VersionPrefix>1.3.0</VersionPrefix> <VersionPrefix>1.4.1</VersionPrefix>
<Authors>ModelContextProtocol</Authors> <Authors>ModelContextProtocol</Authors>
<Copyright>© Model Context Protocol a Series of LF Projects, LLC.</Copyright> <Copyright>© Model Context Protocol a Series of LF Projects, LLC.</Copyright>
<PackageTags>ModelContextProtocol;mcp;ai;llm</PackageTags> <PackageTags>ModelContextProtocol;mcp;ai;llm</PackageTags>

View file

@ -209,7 +209,20 @@ internal sealed class StreamableHttpHandler(
} }
var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString(); var sessionId = context.Request.Headers[McpSessionIdHeaderName].ToString();
if (sessionManager.TryRemove(sessionId, out var session)) if (string.IsNullOrEmpty(sessionId) || !sessionManager.TryGetValue(sessionId, out var session))
{
return;
}
if (!session.HasSameUserId(context.User))
{
await WriteJsonRpcErrorAsync(context,
"Forbidden: The currently authenticated user does not match the user who initiated the session.",
StatusCodes.Status403Forbidden);
return;
}
if (sessionManager.TryRemove(sessionId, out session))
{ {
await session.DisposeAsync(); await session.DisposeAsync();
} }

View file

@ -0,0 +1,32 @@
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Options for exchanging a JWT Authorization Grant for an access token via RFC 7523.
/// </summary>
internal sealed class ExchangeJwtBearerGrantOptions
{
/// <summary>
/// Gets or sets the MCP Server's authorization server token endpoint URL.
/// </summary>
public required string TokenEndpoint { get; set; }
/// <summary>
/// Gets or sets the JWT Authorization Grant (JAG) assertion obtained from token exchange.
/// </summary>
public required string Assertion { get; set; }
/// <summary>
/// Gets or sets the client ID for authentication with the MCP authorization server.
/// </summary>
public required string ClientId { get; set; }
/// <summary>
/// Gets or sets the client secret for authentication with the MCP authorization server. Optional.
/// </summary>
public string? ClientSecret { get; set; }
/// <summary>
/// Gets or sets the scopes to request (space-separated). Optional.
/// </summary>
public string? Scope { get; set; }
}

View file

@ -0,0 +1,296 @@
using System.Net.Http.Headers;
using System.Text.Json;
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Provides internal utilities for the Cross-Application Access authorization flow.
/// </summary>
/// <remarks>
/// Implements the Enterprise Managed Authorization flow as specified at
/// <see href="https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx"/>.
/// </remarks>
internal static class IdentityAssertionGrant
{
#region Constants
/// <summary>Grant type URN for RFC 8693 token exchange.</summary>
public const string GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange";
/// <summary>Grant type URN for RFC 7523 JWT Bearer authorization grant.</summary>
public const string GrantTypeJwtBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer";
/// <summary>Token type URN for OpenID Connect ID Tokens (RFC 8693).</summary>
public const string TokenTypeIdToken = "urn:ietf:params:oauth:token-type:id_token";
/// <summary>Token type URN for SAML 2.0 assertions (RFC 8693).</summary>
public const string TokenTypeSaml2 = "urn:ietf:params:oauth:token-type:saml2";
/// <summary>
/// Token type URN for Identity Assertion JWT Authorization Grants.
/// As specified at
/// <see href="https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx"/>.
/// </summary>
public const string TokenTypeIdJag = "urn:ietf:params:oauth:token-type:id-jag";
/// <summary>
/// The expected value for <c>token_type</c> in a JAG token exchange response per RFC 8693 §2.2.1.
/// The issued token is not an OAuth access token, so its type is "N_A".
/// </summary>
public const string TokenTypeNotApplicable = "N_A";
#endregion
#region Token Exchange (RFC 8693)
/// <summary>
/// Requests a JWT Authorization Grant (JAG) from an Identity Provider via RFC 8693 Token Exchange.
/// Returns the JAG string to be used as a JWT Bearer assertion (RFC 7523) against the MCP authorization server.
/// </summary>
public static async Task<string> RequestJwtAuthorizationGrantAsync(
RequestJwtAuthGrantOptions options,
HttpClient httpClient,
CancellationToken cancellationToken = default)
{
Throw.IfNull(options);
Throw.IfNullOrWhiteSpace(options.TokenEndpoint);
Throw.IfNullOrWhiteSpace(options.Audience);
Throw.IfNullOrWhiteSpace(options.Resource);
Throw.IfNullOrWhiteSpace(options.IdToken);
Throw.IfNullOrWhiteSpace(options.ClientId);
var formData = new Dictionary<string, string>
{
["grant_type"] = GrantTypeTokenExchange,
["requested_token_type"] = TokenTypeIdJag,
["subject_token"] = options.IdToken,
["subject_token_type"] = TokenTypeIdToken,
["audience"] = options.Audience,
["resource"] = options.Resource,
["client_id"] = options.ClientId,
};
if (!string.IsNullOrEmpty(options.ClientSecret))
{
formData["client_secret"] = options.ClientSecret!;
}
if (!string.IsNullOrEmpty(options.Scope))
{
formData["scope"] = options.Scope!;
}
using var requestContent = new FormUrlEncodedContent(formData);
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, options.TokenEndpoint)
{
Content = requestContent
};
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using var httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
var responseBody = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (!httpResponse.IsSuccessStatusCode)
{
OAuthErrorResponse? errorResponse = null;
try
{
errorResponse = JsonSerializer.Deserialize(responseBody, McpJsonUtilities.JsonContext.Default.OAuthErrorResponse);
}
catch
{
// Could not parse error response
}
throw new IdentityAssertionGrantException(
$"Token exchange failed with status {(int)httpResponse.StatusCode}.",
errorResponse?.Error,
errorResponse?.ErrorDescription,
errorResponse?.ErrorUri);
}
var response = JsonSerializer.Deserialize(responseBody, McpJsonUtilities.JsonContext.Default.JagTokenExchangeResponse);
if (response is null)
{
var ex = new IdentityAssertionGrantException("Failed to parse token exchange response.");
ex.Data["ResponseBody"] = responseBody;
throw ex;
}
if (string.IsNullOrEmpty(response.AccessToken))
{
throw new IdentityAssertionGrantException("Token exchange response missing required field: access_token");
}
if (!string.Equals(response.IssuedTokenType, TokenTypeIdJag, StringComparison.Ordinal))
{
throw new IdentityAssertionGrantException(
$"Token exchange response issued_token_type must be '{TokenTypeIdJag}', got '{response.IssuedTokenType}'.");
}
if (!string.Equals(response.TokenType, TokenTypeNotApplicable, StringComparison.Ordinal))
{
throw new IdentityAssertionGrantException(
$"Token exchange response token_type must be '{TokenTypeNotApplicable}' per RFC 8693 §2.2.1, got '{response.TokenType}'.");
}
return response.AccessToken;
}
#endregion
#region JWT Bearer Grant (RFC 7523)
/// <summary>
/// Exchanges a JWT Authorization Grant (JAG) for an access token at an MCP Server's authorization server
/// using the JWT Bearer grant (RFC 7523).
/// </summary>
public static async Task<TokenContainer> ExchangeJwtBearerGrantAsync(
ExchangeJwtBearerGrantOptions options,
HttpClient httpClient,
CancellationToken cancellationToken = default)
{
Throw.IfNull(options);
Throw.IfNullOrWhiteSpace(options.TokenEndpoint);
Throw.IfNullOrWhiteSpace(options.Assertion);
Throw.IfNullOrWhiteSpace(options.ClientId);
var formData = new Dictionary<string, string>
{
["grant_type"] = GrantTypeJwtBearer,
["assertion"] = options.Assertion,
["client_id"] = options.ClientId,
};
if (!string.IsNullOrEmpty(options.ClientSecret))
{
formData["client_secret"] = options.ClientSecret!;
}
if (!string.IsNullOrEmpty(options.Scope))
{
formData["scope"] = options.Scope!;
}
using var requestContent = new FormUrlEncodedContent(formData);
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, options.TokenEndpoint)
{
Content = requestContent
};
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using var httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
var responseBody = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (!httpResponse.IsSuccessStatusCode)
{
OAuthErrorResponse? errorResponse = null;
try
{
errorResponse = JsonSerializer.Deserialize(responseBody, McpJsonUtilities.JsonContext.Default.OAuthErrorResponse);
}
catch
{
// Could not parse error response
}
throw new IdentityAssertionGrantException(
$"JWT bearer grant failed with status {(int)httpResponse.StatusCode}.",
errorResponse?.Error,
errorResponse?.ErrorDescription,
errorResponse?.ErrorUri);
}
var response = JsonSerializer.Deserialize(responseBody, McpJsonUtilities.JsonContext.Default.JwtBearerAccessTokenResponse);
if (response is null)
{
var ex = new IdentityAssertionGrantException("Failed to parse JWT bearer grant response.");
ex.Data["ResponseBody"] = responseBody;
throw ex;
}
if (string.IsNullOrEmpty(response.AccessToken))
{
throw new IdentityAssertionGrantException("JWT bearer grant response missing required field: access_token");
}
if (string.IsNullOrEmpty(response.TokenType))
{
throw new IdentityAssertionGrantException("JWT bearer grant response missing required field: token_type");
}
if (!string.Equals(response.TokenType, "bearer", StringComparison.OrdinalIgnoreCase))
{
throw new IdentityAssertionGrantException(
$"JWT bearer grant response token_type must be 'bearer' per RFC 7523, got '{response.TokenType}'.");
}
return new TokenContainer
{
AccessToken = response.AccessToken,
TokenType = response.TokenType,
RefreshToken = response.RefreshToken,
ExpiresIn = response.ExpiresIn,
Scope = response.Scope,
ObtainedAt = DateTimeOffset.UtcNow,
};
}
#endregion
#region Helper: Auth Server Metadata Discovery
private static readonly string[] s_wellKnownPaths = [".well-known/openid-configuration", ".well-known/oauth-authorization-server"];
/// <summary>
/// Discovers authorization server metadata from the well-known endpoints.
/// </summary>
internal static async Task<AuthorizationServerMetadata> DiscoverAuthServerMetadataAsync(
Uri issuerUrl,
HttpClient httpClient,
CancellationToken cancellationToken)
{
var baseUrl = issuerUrl.ToString();
if (!baseUrl.EndsWith("/", StringComparison.Ordinal))
{
issuerUrl = new Uri($"{baseUrl}/");
}
foreach (var path in s_wellKnownPaths)
{
try
{
var wellKnownEndpoint = new Uri(issuerUrl, path);
var response = await httpClient.GetAsync(wellKnownEndpoint, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
continue;
}
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var metadata = await JsonSerializer.DeserializeAsync(
stream,
McpJsonUtilities.JsonContext.Default.AuthorizationServerMetadata,
cancellationToken).ConfigureAwait(false);
if (metadata is not null)
{
return metadata;
}
}
catch
{
continue;
}
}
throw new IdentityAssertionGrantException($"Failed to discover authorization server metadata for: {issuerUrl}");
}
#endregion
}

View file

@ -0,0 +1,20 @@
namespace ModelContextProtocol.Authentication;
/// <summary>
/// Context provided to the <see cref="IdentityAssertionGrantIdTokenCallback"/> for a Cross-Application Access
/// authorization flow. Contains the URLs discovered during the OAuth flow needed for the token exchange step.
/// </summary>
public sealed class IdentityAssertionGrantContext
{
/// <summary>
/// Gets the MCP resource server URL (i.e., the <c>resource</c> parameter for token exchange).
/// This is the URL of the MCP server being accessed.
/// </summary>
public required Uri ResourceUrl { get; init; }
/// <summary>
/// Gets the MCP authorization server URL (i.e., the <c>audience</c> parameter for token exchange).
/// This is the URL of the authorization server protecting the MCP resource.
/// </summary>
public required Uri AuthorizationServerUrl { get; init; }
}

Some files were not shown because too many files have changed in this diff Show more