Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-code-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
pattern: testresults-*

- name: Combine coverage reports
uses: danielpalme/ReportGenerator-GitHub-Action@5.5.5
uses: danielpalme/ReportGenerator-GitHub-Action@7ae927204961589fcb0b0be245c51fbbc87cbca2 # 5.5.5
with:
reports: "**/*.cobertura.xml"
targetdir: "${{ github.workspace }}/report"
Expand Down
4 changes: 2 additions & 2 deletions docs/concepts/tasks/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

[task-based execution]: https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks
[task-based execution]: https://modelcontextprotocol.io/seps/1686-tasks

## Overview

Expand Down Expand Up @@ -601,4 +601,4 @@ While this file-based approach demonstrates the pattern, production systems shou
- <xref:ModelContextProtocol.InMemoryMcpTaskStore>
- <xref:ModelContextProtocol.Protocol.McpTask>
- <xref:ModelContextProtocol.Protocol.McpTaskStatus>
- [MCP Tasks Specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks)
- [MCP Tasks Specification](https://modelcontextprotocol.io/seps/1686-tasks)
54 changes: 53 additions & 1 deletion docs/concepts/transports/transports.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,63 @@ Key <xref:ModelContextProtocol.Client.StdioClientTransportOptions> properties:
| `Command` | The executable to launch (required) |
| `Arguments` | Command-line arguments for the 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) |
| `StandardErrorLines` | Callback for stderr output from the server process |
| `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

Use <xref:ModelContextProtocol.Server.StdioServerTransport> for servers that communicate over stdin/stdout:
Expand Down
2 changes: 1 addition & 1 deletion docs/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T

| 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). |

## Obsolete APIs
Expand Down
4 changes: 3 additions & 1 deletion samples/ChatWithTools/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
Command = "npx",
Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-everything"],
Name = "Everything",
InheritEnvironmentVariables = false,
EnvironmentVariables = StdioClientTransportOptions.GetDefaultEnvironmentVariables(),
}),
clientOptions: new()
{
Expand Down Expand Up @@ -82,4 +84,4 @@
Console.WriteLine();

messages.AddMessages(updates);
}
}
20 changes: 20 additions & 0 deletions samples/QuickstartClient/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
Name = "Demo Server",
Command = command,
Arguments = arguments,
InheritEnvironmentVariables = false,
EnvironmentVariables = GetMinimalDotNetEnvironment(),
});
}
await using var mcpClient = await McpClient.CreateAsync(clientTransport!);
Expand Down Expand Up @@ -122,3 +124,21 @@ static string GetCurrentSourceDirectory([CallerFilePath] string? currentFile = n
Debug.Assert(!string.IsNullOrWhiteSpace(currentFile));
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;
}
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<PackageProjectUrl>https://csharp.sdk.modelcontextprotocol.io</PackageProjectUrl>
<RepositoryUrl>https://github.com/modelcontextprotocol/csharp-sdk</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<VersionPrefix>1.3.0</VersionPrefix>
<VersionPrefix>1.4.0</VersionPrefix>
<Authors>ModelContextProtocol</Authors>
<Copyright>© Model Context Protocol a Series of LF Projects, LLC.</Copyright>
<PackageTags>ModelContextProtocol;mcp;ai;llm</PackageTags>
Expand Down
15 changes: 14 additions & 1 deletion src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,20 @@ public async Task HandleDeleteRequestAsync(HttpContext context)
}

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();
}
Expand Down
12 changes: 8 additions & 4 deletions src/ModelContextProtocol.Core/Client/StdioClientTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ public async Task<ITransport> ConnectAsync(CancellationToken cancellationToken =
#endif
}

if (!_options.InheritEnvironmentVariables)
{
startInfo.Environment.Clear();
}

if (_options.EnvironmentVariables != null)
{
foreach (var entry in _options.EnvironmentVariables)
Expand All @@ -121,9 +126,8 @@ public async Task<ITransport> ConnectAsync(CancellationToken cancellationToken =

if (logger.IsEnabled(LogLevel.Trace))
{
LogCreateProcessForTransportSensitive(logger, endpointName, _options.Command,
LogCreateProcessForTransportDetailed(logger, endpointName, _options.Command,
startInfo.Arguments,
string.Join(", ", startInfo.Environment.Select(kvp => $"{kvp.Key}={kvp.Value}")),
startInfo.WorkingDirectory);
}
else
Expand Down Expand Up @@ -295,8 +299,8 @@ private static string EscapeArgumentString(string argument) =>
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} starting server process. Command: '{Command}'.")]
private static partial void LogCreateProcessForTransport(ILogger logger, string endpointName, string command);

[LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} starting server process. Command: '{Command}', Arguments: {Arguments}, Environment: {Environment}, Working directory: {WorkingDirectory}.")]
private static partial void LogCreateProcessForTransportSensitive(ILogger logger, string endpointName, string command, string? arguments, string environment, string workingDirectory);
[LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} starting server process. Command: '{Command}', Arguments: {Arguments}, Working directory: {WorkingDirectory}.")]
private static partial void LogCreateProcessForTransportDetailed(ILogger logger, string endpointName, string command, string? arguments, string workingDirectory);

[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} failed to start server process.")]
private static partial void LogTransportProcessStartFailed(ILogger logger, string endpointName);
Expand Down
Loading
Loading