Skip to content

Commit d34fac4

Browse files
halter73jeffhandley
authored andcommitted
Add InheritEnvironmentVariables to StdioClientTransportOptions (#1563)
1 parent 4498146 commit d34fac4

6 files changed

Lines changed: 395 additions & 6 deletions

File tree

docs/concepts/transports/transports.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,63 @@ Key <xref:ModelContextProtocol.Client.StdioClientTransportOptions> properties:
3939
| `Command` | The executable to launch (required) |
4040
| `Arguments` | Command-line arguments for the process |
4141
| `WorkingDirectory` | Working directory for the server process |
42-
| `EnvironmentVariables` | Environment variables (merged with current; `null` values remove variables) |
42+
| `EnvironmentVariables` | Environment variables (merged with current when inheriting; `null` values remove variables) |
43+
| `InheritEnvironmentVariables` | Whether the server process inherits the current process's environment variables (default: `true`) |
4344
| `ShutdownTimeout` | Graceful shutdown timeout (default: 5 seconds) |
4445
| `StandardErrorLines` | Callback for stderr output from the server process |
4546
| `Name` | Optional transport identifier for logging |
4647

48+
#### Environment variable inheritance
49+
50+
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:
51+
52+
```csharp
53+
var transport = new StdioClientTransport(new StdioClientTransportOptions
54+
{
55+
Command = "my-mcp-server",
56+
InheritEnvironmentVariables = false,
57+
EnvironmentVariables = StdioClientTransportOptions.GetDefaultEnvironmentVariables(),
58+
});
59+
```
60+
61+
`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:
62+
63+
```csharp
64+
var env = StdioClientTransportOptions.GetDefaultEnvironmentVariables();
65+
env["MY_SERVER_API_KEY"] = apiKey;
66+
67+
var transport = new StdioClientTransport(new StdioClientTransportOptions
68+
{
69+
Command = "my-mcp-server",
70+
InheritEnvironmentVariables = false,
71+
EnvironmentVariables = env,
72+
});
73+
```
74+
75+
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:
76+
77+
```csharp
78+
var env = new Dictionary<string, string?>();
79+
foreach (var name in new[] { "PATH", "HOME", "HTTP_PROXY", "HTTPS_PROXY" })
80+
{
81+
var value = Environment.GetEnvironmentVariable(name);
82+
if (value is not null)
83+
env[name] = value;
84+
}
85+
86+
var transport = new StdioClientTransport(new StdioClientTransportOptions
87+
{
88+
Command = "my-mcp-server",
89+
InheritEnvironmentVariables = false,
90+
EnvironmentVariables = env,
91+
});
92+
```
93+
94+
> [!WARNING]
95+
> **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.
96+
>
97+
> **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.
98+
4799
#### stdio server
48100

49101
Use <xref:ModelContextProtocol.Server.StdioServerTransport> for servers that communicate over stdin/stdout:

samples/ChatWithTools/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
Command = "npx",
4040
Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-everything"],
4141
Name = "Everything",
42+
InheritEnvironmentVariables = false,
43+
EnvironmentVariables = StdioClientTransportOptions.GetDefaultEnvironmentVariables(),
4244
}),
4345
clientOptions: new()
4446
{
@@ -82,4 +84,4 @@
8284
Console.WriteLine();
8385

8486
messages.AddMessages(updates);
85-
}
87+
}

samples/QuickstartClient/Program.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
Name = "Demo Server",
3232
Command = command,
3333
Arguments = arguments,
34+
InheritEnvironmentVariables = false,
35+
EnvironmentVariables = GetMinimalDotNetEnvironment(),
3436
});
3537
}
3638
await using var mcpClient = await McpClient.CreateAsync(clientTransport!);
@@ -122,3 +124,21 @@ static string GetCurrentSourceDirectory([CallerFilePath] string? currentFile = n
122124
Debug.Assert(!string.IsNullOrWhiteSpace(currentFile));
123125
return Path.GetDirectoryName(currentFile) ?? throw new InvalidOperationException("Unable to determine source directory.");
124126
}
127+
128+
// Returns the safe default environment variables plus extras needed by 'dotnet run'.
129+
// Omitting variables the server doesn't need prevents unintentional leakage of
130+
// credentials or other sensitive values present in the parent process.
131+
static Dictionary<string, string?> GetMinimalDotNetEnvironment()
132+
{
133+
var env = StdioClientTransportOptions.GetDefaultEnvironmentVariables();
134+
// 'dotnet run' also needs DOTNET_ROOT and NUGET_PACKAGES to find the .NET runtime and package cache.
135+
foreach (var key in (string[])["DOTNET_ROOT", "NUGET_PACKAGES"])
136+
{
137+
var value = Environment.GetEnvironmentVariable(key);
138+
if (value is not null)
139+
{
140+
env[key] = value;
141+
}
142+
}
143+
return env;
144+
}

src/ModelContextProtocol.Core/Client/StdioClientTransport.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ public async Task<ITransport> ConnectAsync(CancellationToken cancellationToken =
111111
#endif
112112
}
113113

114+
if (!_options.InheritEnvironmentVariables)
115+
{
116+
startInfo.Environment.Clear();
117+
}
118+
114119
if (_options.EnvironmentVariables != null)
115120
{
116121
foreach (var entry in _options.EnvironmentVariables)

src/ModelContextProtocol.Core/Client/StdioClientTransportOptions.cs

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,96 @@
1+
using System.Runtime.InteropServices;
2+
13
namespace ModelContextProtocol.Client;
24

35
/// <summary>
46
/// Provides options for configuring <see cref="StdioClientTransport"/> instances.
57
/// </summary>
68
public sealed class StdioClientTransportOptions
79
{
10+
// Platform-appropriate allowlists, aligned with the TypeScript and Python MCP SDK defaults.
11+
// TypeScript adds PROGRAMFILES; Python adds PATHEXT. Both are included here.
12+
private static readonly string[] s_defaultWindowsVars =
13+
[
14+
"APPDATA", "HOMEDRIVE", "HOMEPATH", "LOCALAPPDATA", "PATH", "PATHEXT",
15+
"PROCESSOR_ARCHITECTURE", "PROGRAMFILES", "SYSTEMDRIVE", "SYSTEMROOT",
16+
"TEMP", "USERNAME", "USERPROFILE",
17+
];
18+
19+
private static readonly string[] s_defaultUnixVars =
20+
[
21+
"HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER",
22+
];
23+
24+
/// <summary>
25+
/// Returns a curated set of environment variables from the current process that are safe to forward to a child
26+
/// MCP server process.
27+
/// </summary>
28+
/// <returns>
29+
/// A new <see cref="Dictionary{TKey, TValue}"/> populated with the subset of the current process's environment
30+
/// variables that most child processes need to start correctly — for example <c>PATH</c>, <c>HOME</c>, and
31+
/// standard system directories. Values that appear to be shell function definitions (those starting with
32+
/// <c>()</c>) are excluded for security reasons.
33+
/// </returns>
34+
/// <remarks>
35+
/// <para>
36+
/// The allowlist is aligned with the defaults used by the TypeScript and Python MCP SDKs. On Windows it
37+
/// includes: <c>APPDATA</c>, <c>HOMEDRIVE</c>, <c>HOMEPATH</c>, <c>LOCALAPPDATA</c>, <c>PATH</c>,
38+
/// <c>PATHEXT</c>, <c>PROCESSOR_ARCHITECTURE</c>, <c>PROGRAMFILES</c>, <c>SYSTEMDRIVE</c>,
39+
/// <c>SYSTEMROOT</c>, <c>TEMP</c>, <c>USERNAME</c>, and <c>USERPROFILE</c>. On Unix/macOS it includes:
40+
/// <c>HOME</c>, <c>LOGNAME</c>, <c>PATH</c>, <c>SHELL</c>, <c>TERM</c>, and <c>USER</c>.
41+
/// </para>
42+
/// <para>
43+
/// This method is designed to be used together with <see cref="InheritEnvironmentVariables"/> set to
44+
/// <see langword="false"/>. Pass the returned dictionary as <see cref="EnvironmentVariables"/>, optionally
45+
/// adding any server-specific variables the server requires:
46+
/// <code>
47+
/// var env = StdioClientTransportOptions.GetDefaultEnvironmentVariables();
48+
/// env["MY_SERVER_API_KEY"] = apiKey;
49+
///
50+
/// var transport = new StdioClientTransport(new StdioClientTransportOptions
51+
/// {
52+
/// Command = "my-mcp-server",
53+
/// InheritEnvironmentVariables = false,
54+
/// EnvironmentVariables = env,
55+
/// });
56+
/// </code>
57+
/// </para>
58+
/// <para>
59+
/// If the server requires additional variables not in the default set (such as <c>DOTNET_ROOT</c>,
60+
/// <c>JAVA_HOME</c>, or proxy settings), add them explicitly after calling this method.
61+
/// </para>
62+
/// </remarks>
63+
public static Dictionary<string, string?> GetDefaultEnvironmentVariables()
64+
{
65+
var names = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
66+
? s_defaultWindowsVars
67+
: s_defaultUnixVars;
68+
69+
var result = new Dictionary<string, string?>(
70+
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
71+
? StringComparer.OrdinalIgnoreCase
72+
: StringComparer.Ordinal);
73+
foreach (var name in names)
74+
{
75+
var value = Environment.GetEnvironmentVariable(name);
76+
if (value is null)
77+
{
78+
continue;
79+
}
80+
81+
if (value.StartsWith("()", StringComparison.Ordinal))
82+
{
83+
// Skip shell function definitions — they are a security risk.
84+
continue;
85+
}
86+
87+
result[name] = value;
88+
}
89+
90+
return result;
91+
}
92+
93+
894
/// <summary>
995
/// Gets or sets the command to execute to start the server process.
1096
/// </summary>
@@ -38,6 +124,44 @@ public required string Command
38124
/// </summary>
39125
public string? WorkingDirectory { get; set; }
40126

127+
/// <summary>
128+
/// Gets or sets a value indicating whether the server process should inherit the current process's environment variables.
129+
/// </summary>
130+
/// <value>
131+
/// <see langword="true"/> to inherit the current process's environment variables (the default); <see langword="false"/>
132+
/// to start the server process with an empty environment and only the variables explicitly provided via
133+
/// <see cref="EnvironmentVariables"/>.
134+
/// </value>
135+
/// <remarks>
136+
/// <para>
137+
/// When <see langword="true"/> (the default), the server process starts with all of the current process's environment
138+
/// variables. Any entries in <see cref="EnvironmentVariables"/> are then applied on top, adding or overwriting inherited
139+
/// variables.
140+
/// </para>
141+
/// <para>
142+
/// When <see langword="false"/>, the server process starts with a completely empty environment. The <see cref="EnvironmentVariables"/>
143+
/// dictionary is the sole source of environment variables for the child process. This is useful when you want to minimize
144+
/// the attack surface by preventing credentials, tokens, proxy settings, and other sensitive values present in the current
145+
/// environment from unintentionally reaching the child process.
146+
/// </para>
147+
/// <para>
148+
/// <strong>Security consideration:</strong> Inheriting environment variables (the default) can unintentionally expose
149+
/// sensitive values to the child process. Variables such as <c>AWS_SECRET_ACCESS_KEY</c>, <c>GITHUB_TOKEN</c>,
150+
/// <c>OPENAI_API_KEY</c>, and similar credentials that are present in the parent process will automatically flow into
151+
/// the server process, which may be undesirable when running third-party or untrusted MCP servers.
152+
/// </para>
153+
/// <para>
154+
/// <strong>Compatibility consideration:</strong> Disabling inheritance can cause the child process to fail to start or
155+
/// behave unexpectedly if it relies on variables provided by the operating system or the user's shell environment.
156+
/// <see cref="GetDefaultEnvironmentVariables"/> covers the most common requirements — <c>PATH</c>, <c>HOME</c>, and
157+
/// standard system directories — and is a safe starting point for most servers. For servers that also need variables
158+
/// outside that set (such as <c>DOTNET_ROOT</c>, <c>LD_LIBRARY_PATH</c>, <c>JAVA_HOME</c>, or proxy settings like
159+
/// <c>HTTP_PROXY</c>, <c>HTTPS_PROXY</c>, and <c>NO_PROXY</c>), add them explicitly via <see cref="EnvironmentVariables"/>
160+
/// after calling <see cref="GetDefaultEnvironmentVariables"/>.
161+
/// </para>
162+
/// </remarks>
163+
public bool InheritEnvironmentVariables { get; set; } = true;
164+
41165
/// <summary>
42166
/// Gets or sets environment variables to set for the server process.
43167
/// </summary>
@@ -48,10 +172,14 @@ public required string Command
48172
/// to the server without modifying its code.
49173
/// </para>
50174
/// <para>
51-
/// By default, when starting the server process, the server process will inherit the current environment's variables,
52-
/// as discovered via <see cref="Environment.GetEnvironmentVariables()"/>. After those variables are found, the entries
53-
/// in this <see cref="EnvironmentVariables"/> dictionary are used to augment and overwrite the entries read from the environment.
54-
/// That includes removing the variables for any of this collection's entries with a null value.
175+
/// When <see cref="InheritEnvironmentVariables"/> is <see langword="true"/> (the default), the server process starts with
176+
/// all environment variables inherited from the current process. The entries in this <see cref="EnvironmentVariables"/>
177+
/// dictionary are then applied on top: adding new variables, overwriting inherited ones, or removing variables whose
178+
/// value is set to <see langword="null"/>.
179+
/// </para>
180+
/// <para>
181+
/// When <see cref="InheritEnvironmentVariables"/> is <see langword="false"/>, the server process starts with an empty
182+
/// environment. This dictionary is the sole source of environment variables for the child process.
55183
/// </para>
56184
/// </remarks>
57185
public IDictionary<string, string?>? EnvironmentVariables { get; set; }

0 commit comments

Comments
 (0)