This repository was archived by the owner on May 29, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 19
This repository was archived by the owner on May 29, 2025. It is now read-only.
The Environment Variable sent in the initializer are not added to the process. #16
Copy link
Copy link
Closed
Description
The Environment Variable Issue
Context
Trying to add an API Key for a mcp server like for brave search
{
"mcpServers": {
"brave-search": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-brave-search"
],
"env": {
"BRAVE_API_KEY": "YOUR_API_KEY_HERE"
}
}
}
}
The Error
When initializing your MCPClient, the environment variable BRAVE_API_KEY wasn't being respected, causing this error:
Error: BRAVE_API_KEY environment variable is required.
Why It Happened
Looking at the original loadZshEnvironment implementation:
private static func loadZshEnvironment() throws -> [String: String] {
let process = Process()
process.launchPath = "/bin/zsh"
process.arguments = ["-c", "source ~/.zshenv; source ~/.zprofile; source ~/.zshrc; source ~/.zshrc; printenv"]
let env = try getProcessStdout(process: process)
if let path = env?.split(separator: "\n").filter({ $0.starts(with: "PATH=") }).last {
return ["PATH": String(path.dropFirst("PATH=".count))]
} else {
return ProcessInfo.processInfo.environment
}
}This function had two critical flaws:
- It returned ONLY the PATH variable from the shell environment (
return ["PATH": String(path)]), discarding all other environment variables - It didn't accept or merge user-provided variables like
BRAVE_API_KEY
The potential Solution
private static func loadZshEnvironment(userEnv: [String: String]? = nil) throws -> [String: String] {
// Load full shell environment
let shellProcess = Process()
shellProcess.executableURL = URL(fileURLWithPath: "/bin/zsh")
shellProcess.arguments = ["-ilc", "printenv"] // Interactive login shell
let outputPipe = Pipe()
shellProcess.standardOutput = outputPipe
try shellProcess.run()
shellProcess.waitUntilExit()
// Parse ALL environment variables (not just PATH)
var mergedEnv: [String: String] = [:]
if let outputString = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) {
outputString.split(separator: "\n").forEach { line in
let components = line.split(separator: "=", maxSplits: 1)
if components.count == 2 {
mergedEnv[String(components[0])] = String(components[1])
}
}
}
// Give priority to user-provided variables
userEnv?.forEach { key, value in
mergedEnv[key] = value
}
return mergedEnv
}This works because:
- We use
-ilcflags to load a complete interactive login shell environment - We capture ALL environment variables, not just PATH
- We accept
userEnvas a parameter and overlay these values with priority - We preserve the original PATH from the shell, ensuring commands like
npxare found
When you initialize your client with env: ["BRAVE_API_KEY": "..."], this value will be properly included in the final environment.
Complete Changes suggested by Claude:
private static func loadZshEnvironment(userEnv: [String: String]? = nil) throws -> [String: String] {
// Load shell environment as base
let shellProcess = Process()
shellProcess.executableURL = URL(fileURLWithPath: "/bin/zsh")
shellProcess.arguments = ["-ilc", "printenv"]
let outputPipe = Pipe()
shellProcess.standardOutput = outputPipe
shellProcess.standardError = Pipe()
try shellProcess.run()
shellProcess.waitUntilExit()
let data = outputPipe.fileHandleForReading.readDataToEndOfFile()
guard let outputString = String(data: data, encoding: .utf8) else {
logger.error("Failed to read environment from shell.")
return ProcessInfo.processInfo.environment
}
// Parse shell environment
var mergedEnv: [String: String] = [:]
outputString.split(separator: "\n").forEach { line in
let components = line.split(separator: "=", maxSplits: 1)
guard components.count == 2 else { return }
let key = String(components[0])
let value = String(components[1])
mergedEnv[key] = value
}
// Overlay user-defined environment variables explicitly
userEnv?.forEach { key, value in
mergedEnv[key] = value
}
// Log for debugging clarity
logger.debug("Final merged environment: \(mergedEnv)")
return mergedEnv
}And
public static func stdioProcess(
_ executable: String,
args: [String] = [],
cwd: String? = nil,
env: [String: String]? = nil,
verbose: Bool = false)
throws -> Transport
{
if verbose {
let command = "\(executable) \(args.joined(separator: " "))"
logger.log("Running ↪ \(command)")
}
// Create the process
func path(for executable: String, env: [String: String]?) -> String? {
guard !executable.contains("/") else {
return executable
}
do {
let path = try locate(executable: executable, env: env)
return path.isEmpty ? nil : path
} catch {
// Most likely an error because we could not locate the executable
return nil
}
}
let process = Process()
// In MacOS, zsh is the default since macOS Catalina 10.15.7. We can safely assume it is available.
process.launchPath = "/bin/zsh"
// Load shell environment and merge with user-provided environment
process.environment = try loadZshEnvironment(userEnv: env)
let command = "\(executable) \(args.joined(separator: " "))"
process.arguments = ["-c"] + [command]
// Working directory
if let cwd {
process.currentDirectoryPath = cwd
}
// Input/output
let stdin = Pipe()
let stdout = Pipe()
let stderr = Pipe()
process.standardInput = stdin
process.standardOutput = stdout
process.standardError = stderr
return try stdioProcess(unlaunchedProcess: process, verbose: verbose)
}Metadata
Metadata
Assignees
Labels
No labels