Skip to content
This repository was archived by the owner on May 29, 2025. It is now read-only.
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import Foundation
import JSONRPC
import MCPInterface
import OSLog

private let logger = Logger(
Expand Down Expand Up @@ -42,17 +43,18 @@ extension JSONRPCSetupError: LocalizedError {
}
}

extension DataChannel {
extension Transport {

// MARK: Public

/// Creates a new `Transport` by launching the given executable with the specified arguments and attaching to its standard IO.
public static func stdioProcess(
_ executable: String,
args: [String] = [],
cwd: String? = nil,
env: [String: String]? = nil,
verbose: Bool = false)
throws -> DataChannel
throws -> Transport
{
if verbose {
let command = "\(executable) \(args.joined(separator: " "))"
Expand Down Expand Up @@ -103,10 +105,11 @@ extension DataChannel {
return try stdioProcess(unlaunchedProcess: process, verbose: verbose)
}

/// Creates a new `Transport` by launching the given process and attaching to its standard IO.
public static func stdioProcess(
unlaunchedProcess process: Process,
verbose: Bool = false)
throws -> DataChannel
throws -> Transport
{
guard
let stdin = process.standardInput as? Pipe,
Expand All @@ -119,7 +122,6 @@ extension DataChannel {
// Run the process
var stdoutData = Data()
var stderrData = Data()

let outStream: AsyncStream<Data>
if verbose {
// As we are both reading stdout here in this function, and want to make the stream readable to the caller,
Expand All @@ -131,7 +133,7 @@ extension DataChannel {
}

Task {
for await data in stdout.fileHandleForReading.dataStream {
for await data in stdout.fileHandleForReading.dataStream.jsonStream {
stdoutData.append(data)
outContinuation?.yield(data)

Expand All @@ -150,10 +152,10 @@ extension DataChannel {
}
} else {
// If we are not in verbose mode, we are not reading from stdout internally, so we can just return the stream directly.
outStream = stdout.fileHandleForReading.dataStream
outStream = stdout.fileHandleForReading.dataStream.jsonStream
}

// Ensures that the process is terminated when the DataChannel is de-referenced.
// Ensures that the process is terminated when the Transport is de-referenced.
let lifetime = Lifetime {
if process.isRunning {
process.terminate()
Expand All @@ -177,7 +179,7 @@ extension DataChannel {
throw error
}

let writeHandler: DataChannel.WriteHandler = { [lifetime] data in
let writeHandler: Transport.WriteHandler = { [lifetime] data in
_ = lifetime
if verbose {
logger.log("Sending data:\n\(String(data: data, encoding: .utf8) ?? "nil")")
Expand All @@ -188,7 +190,7 @@ extension DataChannel {
stdin.fileHandleForWriting.write(Data("\n".utf8))
}

return DataChannel(writeHandler: writeHandler, dataSequence: outStream)
return Transport(writeHandler: writeHandler, dataSequence: outStream)
}

// MARK: Private
Expand All @@ -213,10 +215,12 @@ extension DataChannel {
private static func loadZshEnvironment() throws -> [String: String] {
let process = Process()
process.launchPath = "/bin/zsh"
process.arguments = ["-c", "source ~/.zshrc && printenv"]
// Those are loaded for interactive login shell by zsh:
// https://www.freecodecamp.org/news/how-do-zsh-configuration-files-work/
process.arguments = ["-c", "source ~/.zshenv; source ~/.zprofile; source ~/.zshrc; source ~/.zshrc; printenv"]
Copy link
Owner

Choose a reason for hiding this comment

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

nit: below I think looking for last instead of first is more correct, in the unexpected case sourcing one of those file was also printing the PATH. ie

if let path = env?.split(separator: "\n").filter({ $0.starts(with: "PATH=") }).last {

let env = try getProcessStdout(process: process)

if let path = env?.split(separator: "\n").filter({ $0.starts(with: "PATH=") }).first {
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
Expand Down
Loading