Skip to content
Open
35 changes: 34 additions & 1 deletion docs/concepts/transports/transports.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,44 @@ 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to mention anything regarding Windows shell wrapping?

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. 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,
});
```

> [!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
5 changes: 4 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,5 @@
Console.WriteLine();

messages.AddMessages(updates);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra empty lines

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 = MinimalDotNetEnvironment(),
});
}
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?> MinimalDotNetEnvironment()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: call it 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;
}
5 changes: 5 additions & 0 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 Down
136 changes: 132 additions & 4 deletions src/ModelContextProtocol.Core/Client/StdioClientTransportOptions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,96 @@
using System.Runtime.InteropServices;

namespace ModelContextProtocol.Client;

/// <summary>
/// Provides options for configuring <see cref="StdioClientTransport"/> instances.
/// </summary>
public sealed class StdioClientTransportOptions
{
// Platform-appropriate allowlists, aligned with the TypeScript and Python MCP SDK defaults.
// TypeScript adds PROGRAMFILES; Python adds PATHEXT. Both are included here.
private static readonly string[] s_defaultWindowsVars =
[
"APPDATA", "HOMEDRIVE", "HOMEPATH", "LOCALAPPDATA", "PATH", "PATHEXT",
"PROCESSOR_ARCHITECTURE", "PROGRAMFILES", "SYSTEMDRIVE", "SYSTEMROOT",
"TEMP", "USERNAME", "USERPROFILE",
];

private static readonly string[] s_defaultUnixVars =
[
"HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER",
];

/// <summary>
/// Returns a curated set of environment variables from the current process that are safe to forward to a child
/// MCP server process.
/// </summary>
/// <returns>
/// A new <see cref="Dictionary{TKey, TValue}"/> populated with the subset of the current process's environment
/// variables that most child processes need to start correctly — for example <c>PATH</c>, <c>HOME</c>, and
/// standard system directories. Values that appear to be shell function definitions (those starting with
/// <c>()</c>) are excluded for security reasons.
/// </returns>
/// <remarks>
/// <para>
/// The allowlist is aligned with the defaults used by the TypeScript and Python MCP SDKs. On Windows it
/// includes: <c>APPDATA</c>, <c>HOMEDRIVE</c>, <c>HOMEPATH</c>, <c>LOCALAPPDATA</c>, <c>PATH</c>,
/// <c>PATHEXT</c>, <c>PROCESSOR_ARCHITECTURE</c>, <c>PROGRAMFILES</c>, <c>SYSTEMDRIVE</c>,
/// <c>SYSTEMROOT</c>, <c>TEMP</c>, <c>USERNAME</c>, and <c>USERPROFILE</c>. On Unix/macOS it includes:
/// <c>HOME</c>, <c>LOGNAME</c>, <c>PATH</c>, <c>SHELL</c>, <c>TERM</c>, and <c>USER</c>.
/// </para>
/// <para>
/// This method is designed to be used together with <see cref="InheritEnvironmentVariables"/> set to
/// <see langword="false"/>. Pass the returned dictionary as <see cref="EnvironmentVariables"/>, optionally
/// adding any server-specific variables the server requires:
/// <code>
/// var env = StdioClientTransportOptions.GetDefaultEnvironmentVariables();
/// env["MY_SERVER_API_KEY"] = apiKey;
///
/// var transport = new StdioClientTransport(new StdioClientTransportOptions
/// {
/// Command = "my-mcp-server",
/// InheritEnvironmentVariables = false,
/// EnvironmentVariables = env,
/// });
/// </code>
/// </para>
/// <para>
/// If the server requires additional variables not in the default set (such as <c>DOTNET_ROOT</c>,
/// <c>JAVA_HOME</c>, or proxy settings), add them explicitly after calling this method.
/// </para>
/// </remarks>
public static Dictionary<string, string?> GetDefaultEnvironmentVariables()
{
var names = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? s_defaultWindowsVars
: s_defaultUnixVars;

var result = new Dictionary<string, string?>(
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? StringComparer.OrdinalIgnoreCase
: StringComparer.Ordinal);
foreach (var name in names)
{
var value = Environment.GetEnvironmentVariable(name);
if (value is null)
{
continue;
}

if (value.StartsWith("()", StringComparison.Ordinal))
{
// Skip shell function definitions — they are a security risk.
continue;
}

result[name] = value;
}

return result;
}


/// <summary>
/// Gets or sets the command to execute to start the server process.
/// </summary>
Expand Down Expand Up @@ -38,6 +124,44 @@ public required string Command
/// </summary>
public string? WorkingDirectory { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the server process should inherit the current process's environment variables.
/// </summary>
/// <value>
/// <see langword="true"/> to inherit the current process's environment variables (the default); <see langword="false"/>
/// to start the server process with an empty environment and only the variables explicitly provided via
/// <see cref="EnvironmentVariables"/>.
/// </value>
/// <remarks>
/// <para>
/// When <see langword="true"/> (the default), the server process starts with all of the current process's environment
/// variables. Any entries in <see cref="EnvironmentVariables"/> are then applied on top, adding or overwriting inherited
/// variables.
/// </para>
/// <para>
/// When <see langword="false"/>, the server process starts with a completely empty environment. The <see cref="EnvironmentVariables"/>
/// dictionary is the sole source of environment variables for the child process. This is useful when you want to minimize
/// the attack surface by preventing credentials, tokens, proxy settings, and other sensitive values present in the current
/// environment from unintentionally reaching the child process.
/// </para>
/// <para>
/// <strong>Security consideration:</strong> Inheriting environment variables (the default) can unintentionally expose
/// sensitive values to the child process. Variables such as <c>AWS_SECRET_ACCESS_KEY</c>, <c>GITHUB_TOKEN</c>,
/// <c>OPENAI_API_KEY</c>, and similar credentials that are present in the parent process will automatically flow into
/// the server process, which may be undesirable when running third-party or untrusted MCP servers.
/// </para>
/// <para>
/// <strong>Compatibility consideration:</strong> Disabling inheritance can cause the child process to fail to start or
/// behave unexpectedly if it relies on variables provided by the operating system or the user's shell environment.
/// <see cref="GetDefaultEnvironmentVariables"/> covers the most common requirements — <c>PATH</c>, <c>HOME</c>, and
/// standard system directories — and is a safe starting point for most servers. For servers that also need variables
/// outside that set (such as <c>DOTNET_ROOT</c>, <c>LD_LIBRARY_PATH</c>, <c>JAVA_HOME</c>, or proxy settings like
/// <c>HTTP_PROXY</c>, <c>HTTPS_PROXY</c>, and <c>NO_PROXY</c>), add them explicitly via <see cref="EnvironmentVariables"/>
/// after calling <see cref="GetDefaultEnvironmentVariables"/>.
/// </para>
/// </remarks>
public bool InheritEnvironmentVariables { get; set; } = true;

/// <summary>
/// Gets or sets environment variables to set for the server process.
/// </summary>
Expand All @@ -48,10 +172,14 @@ public required string Command
/// to the server without modifying its code.
/// </para>
/// <para>
/// By default, when starting the server process, the server process will inherit the current environment's variables,
/// as discovered via <see cref="Environment.GetEnvironmentVariables()"/>. After those variables are found, the entries
/// in this <see cref="EnvironmentVariables"/> dictionary are used to augment and overwrite the entries read from the environment.
/// That includes removing the variables for any of this collection's entries with a null value.
/// When <see cref="InheritEnvironmentVariables"/> is <see langword="true"/> (the default), the server process starts with
/// all environment variables inherited from the current process. The entries in this <see cref="EnvironmentVariables"/>
/// dictionary are then applied on top: adding new variables, overwriting inherited ones, or removing variables whose
/// value is set to <see langword="null"/>.
/// </para>
/// <para>
/// When <see cref="InheritEnvironmentVariables"/> is <see langword="false"/>, the server process starts with an empty
/// environment. This dictionary is the sole source of environment variables for the child process.
/// </para>
/// </remarks>
public IDictionary<string, string?>? EnvironmentVariables { get; set; }
Expand Down
Loading
Loading