Skip to content
This repository was archived by the owner on May 29, 2025. It is now read-only.

Commit 18e8e03

Browse files
committed
Merge branch 'main' of github.com:gsabran/mcp-swift-sdk into gui--see-ex
2 parents ad9351c + 102b4d7 commit 18e8e03

File tree

75 files changed

+3158
-336
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+3158
-336
lines changed

.github/workflows/swift.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ jobs:
4444
runs-on: macos-15
4545
steps:
4646
- uses: actions/checkout@v4
47+
- name: Set up Homebrew
48+
id: set-up-homebrew
49+
uses: Homebrew/actions/setup-homebrew@master
50+
- name: Install swiftformat
51+
run: brew install swiftformat
4752
- name: Run linter
48-
run: swift package --allow-writing-to-package-directory format
49-
- name: Verify that `swift package --allow-writing-to-package-directory format` did not change outputs (if it did, please re-run it and re-commit!)
53+
run: swiftformat --config rules.swiftformat .
54+
- name: Verify that `swiftformat --config rules.swiftformat .` did not change outputs (if it did, please re-run it and re-commit!)
5055
run: git diff --exit-code

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,4 @@ fastlane/Preview.html
6262
fastlane/screenshots/**/*.png
6363
fastlane/test_output
6464
.DS_Store
65+
.vscode

MCPClient/Sources/MCPClient.swift

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import MCPInterface
88

99
public actor MCPClient: MCPClientInterface {
1010

11-
// MARK: Lifecycle
12-
1311
/// Creates a MCP client and connects to the server through the provided transport.
1412
/// The methods completes after connecting to the server.
1513
public init(
@@ -19,7 +17,7 @@ public actor MCPClient: MCPClientInterface {
1917
async throws {
2018
try await self.init(
2119
capabilities: capabilities,
22-
connection: try MCPClientConnection(
20+
connection: MCPClientConnection(
2321
info: info,
2422
capabilities: ClientCapabilities(
2523
experimental: nil, // TODO: support experimental requests
@@ -47,31 +45,29 @@ public actor MCPClient: MCPClientInterface {
4745
Task { try await self.updateResourceTemplates() }
4846
}
4947

50-
// MARK: Public
51-
5248
public private(set) var serverInfo: ServerInfo
5349

5450
public var tools: ReadOnlyCurrentValueSubject<CapabilityStatus<[Tool]>, Never> {
5551
get async {
56-
await .init(_tools.compactMap { $0 }.removeDuplicates().eraseToAnyPublisher())
52+
await .init(_tools.compactMap(\.self).removeDuplicates().eraseToAnyPublisher())
5753
}
5854
}
5955

6056
public var prompts: ReadOnlyCurrentValueSubject<CapabilityStatus<[Prompt]>, Never> {
6157
get async {
62-
await .init(_prompts.compactMap { $0 }.removeDuplicates().eraseToAnyPublisher())
58+
await .init(_prompts.compactMap(\.self).removeDuplicates().eraseToAnyPublisher())
6359
}
6460
}
6561

6662
public var resources: ReadOnlyCurrentValueSubject<CapabilityStatus<[Resource]>, Never> {
6763
get async {
68-
await .init(_resources.compactMap { $0 }.removeDuplicates().eraseToAnyPublisher())
64+
await .init(_resources.compactMap(\.self).removeDuplicates().eraseToAnyPublisher())
6965
}
7066
}
7167

7268
public var resourceTemplates: ReadOnlyCurrentValueSubject<CapabilityStatus<[ResourceTemplate]>, Never> {
7369
get async {
74-
await .init(_resourceTemplates.compactMap { $0 }.removeDuplicates().eraseToAnyPublisher())
70+
await .init(_resourceTemplates.compactMap(\.self).removeDuplicates().eraseToAnyPublisher())
7571
}
7672
}
7773

@@ -99,7 +95,7 @@ public actor MCPClient: MCPClientInterface {
9995
}
10096
// If there has been an error during the execution, throw it
10197
if result.isError == true {
102-
let errors = result.content.compactMap { $0.text }.map { CallToolResult.ExecutionError(text: $0.text) }
98+
let errors = result.content.compactMap(\.text).map { CallToolResult.ExecutionError(text: $0.text) }
10399
throw MCPClientError.toolCallError(executionErrors: errors)
104100
}
105101
return result
@@ -119,12 +115,8 @@ public actor MCPClient: MCPClientInterface {
119115
return try await connection.readResource(.init(uri: uri))
120116
}
121117

122-
// MARK: Internal
123-
124118
let connection: MCPClientConnectionInterface
125119

126-
// MARK: Private
127-
128120
private let capabilities: ClientCapabilityHandlers
129121

130122
private let _tools = CurrentValueSubject<CapabilityStatus<[Tool]>?, Never>(nil)
@@ -185,7 +177,7 @@ public actor MCPClient: MCPClientInterface {
185177
private func startListeningToRequests() async {
186178
let requests = await connection.requestsToHandle
187179
Task { [weak self] in
188-
for await(request, completion) in requests {
180+
for await (request, completion) in requests {
189181
guard let self else {
190182
completion(.failure(.init(
191183
code: JRPCErrorCodes.internalError.rawValue,
@@ -210,7 +202,7 @@ public actor MCPClient: MCPClientInterface {
210202
{
211203
if let handler {
212204
do {
213-
return .success(try await handler(params))
205+
return try await .success(handler(params))
214206
} catch {
215207
return .failure(.init(
216208
code: JRPCErrorCodes.internalError.rawValue,

MCPClient/Sources/MCPClientConnection.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import MemberwiseInit
77

88
public actor MCPClientConnection: MCPClientConnectionInterface {
99

10-
// MARK: Lifecycle
11-
1210
public init(
1311
info: Implementation,
1412
capabilities: ClientCapabilities,
@@ -21,8 +19,6 @@ public actor MCPClientConnection: MCPClientConnectionInterface {
2119
self.capabilities = capabilities
2220
}
2321

24-
// MARK: Public
25-
2622
public let info: Implementation
2723

2824
public let capabilities: ClientCapabilities
@@ -108,8 +104,6 @@ public actor MCPClientConnection: MCPClientConnectionInterface {
108104
try await jrpcSession.send(RootsListChangedNotification())
109105
}
110106

111-
// MARK: Private
112-
113107
private let _connection: MCPConnection<ServerRequest, ServerNotification>
114108

115109
private var jrpcSession: JSONRPCSession {

MCPClient/Sources/MCPClientInterface.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ extension MCPClientError: LocalizedError {
6565
public var errorDescription: String? {
6666
switch self {
6767
case .versionMismatch(let received, let expected):
68-
return "Version mismatch between server and client. Received: \(received), Expected: \(expected)"
68+
"Version mismatch between server and client. Received: \(received), Expected: \(expected)"
6969
case .toolCallError(let executionErrors):
70-
return "Error executing tool:\n\(executionErrors.map { $0.errorDescription ?? "unknown error" }.joined(separator: "\n\n"))"
70+
"Error executing tool:\n\(executionErrors.map { $0.errorDescription ?? "unknown error" }.joined(separator: "\n\n"))"
7171
}
7272
}
7373
}

MCPClient/Sources/Process+extensions.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ extension Process {
3535
func finish() throws {
3636
/// The full path to the executable + all arguments, each one quoted if it contains a space.
3737
func commandAsString() -> String {
38-
let path: String
39-
if #available(OSX 10.13, *) {
40-
path = self.executableURL?.path ?? ""
41-
} else {
42-
path = launchPath ?? ""
43-
}
38+
let path: String =
39+
if #available(OSX 10.13, *) {
40+
self.executableURL?.path ?? ""
41+
} else {
42+
launchPath ?? ""
43+
}
4444
return (arguments ?? []).reduce(path) { (acc: String, arg: String) in
4545
acc + " " + (arg.contains(" ") ? ("\"" + arg + "\"") : arg)
4646
}
@@ -66,9 +66,9 @@ enum CommandError: Error, Equatable {
6666
public var errorcode: Int {
6767
switch self {
6868
case .returnedErrorCode(_, let code):
69-
return code
69+
code
7070
case .inAccessibleExecutable:
71-
return 127 // according to http://tldp.org/LDP/abs/html/exitcodes.html
71+
127 // according to http://tldp.org/LDP/abs/html/exitcodes.html
7272
}
7373
}
7474
}
@@ -79,9 +79,9 @@ extension CommandError: CustomStringConvertible {
7979
public var description: String {
8080
switch self {
8181
case .inAccessibleExecutable(let path):
82-
return "Could not execute file at path '\(path)'."
82+
"Could not execute file at path '\(path)'."
8383
case .returnedErrorCode(let command, let code):
84-
return "Command '\(command)' returned with error code \(code)."
84+
"Command '\(command)' returned with error code \(code)."
8585
}
8686
}
8787
}

MCPClient/Sources/DataChannel+StdioProcess.swift renamed to MCPClient/Sources/stdioTransport/DataChannel+StdioProcess.swift

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
import Foundation
33
import JSONRPC
4+
import MCPInterface
45
import OSLog
56

67
private let logger = Logger(
@@ -22,37 +23,36 @@ extension JSONRPCSetupError: LocalizedError {
2223
public var errorDescription: String? {
2324
switch self {
2425
case .missingStandardIO:
25-
return "Missing standard IO"
26+
"Missing standard IO"
2627
case .couldNotLocateExecutable(let executable, let error):
27-
return "Could not locate executable \(executable) \(error ?? "")".trimmingCharacters(in: .whitespaces)
28+
"Could not locate executable \(executable) \(error ?? "")".trimmingCharacters(in: .whitespaces)
2829
case .standardIOConnectionError(let message):
29-
return "Could not connect to stdio: \(message)".trimmingCharacters(in: .whitespaces)
30+
"Could not connect to stdio: \(message)".trimmingCharacters(in: .whitespaces)
3031
}
3132
}
3233

3334
public var recoverySuggestion: String? {
3435
switch self {
3536
case .missingStandardIO:
36-
return "Make sure that the Process that is passed as an argument has stdin, stdout and stderr set as a Pipe."
37+
"Make sure that the Process that is passed as an argument has stdin, stdout and stderr set as a Pipe."
3738
case .couldNotLocateExecutable:
38-
return "Check that the executable is findable given the PATH environment variable. If needed, pass the right environment to the process."
39+
"Check that the executable is findable given the PATH environment variable. If needed, pass the right environment to the process."
3940
case .standardIOConnectionError:
40-
return nil
41+
nil
4142
}
4243
}
4344
}
4445

45-
extension DataChannel {
46-
47-
// MARK: Public
46+
extension Transport {
4847

48+
/// Creates a new `Transport` by launching the given executable with the specified arguments and attaching to its standard IO.
4949
public static func stdioProcess(
5050
_ executable: String,
5151
args: [String] = [],
5252
cwd: String? = nil,
5353
env: [String: String]? = nil,
5454
verbose: Bool = false)
55-
throws -> DataChannel
55+
throws -> Transport
5656
{
5757
if verbose {
5858
let command = "\(executable) \(args.joined(separator: " "))"
@@ -103,10 +103,11 @@ extension DataChannel {
103103
return try stdioProcess(unlaunchedProcess: process, verbose: verbose)
104104
}
105105

106+
/// Creates a new `Transport` by launching the given process and attaching to its standard IO.
106107
public static func stdioProcess(
107108
unlaunchedProcess process: Process,
108109
verbose: Bool = false)
109-
throws -> DataChannel
110+
throws -> Transport
110111
{
111112
guard
112113
let stdin = process.standardInput as? Pipe,
@@ -119,7 +120,6 @@ extension DataChannel {
119120
// Run the process
120121
var stdoutData = Data()
121122
var stderrData = Data()
122-
123123
let outStream: AsyncStream<Data>
124124
if verbose {
125125
// As we are both reading stdout here in this function, and want to make the stream readable to the caller,
@@ -131,7 +131,7 @@ extension DataChannel {
131131
}
132132

133133
Task {
134-
for await data in stdout.fileHandleForReading.dataStream {
134+
for await data in stdout.fileHandleForReading.dataStream.jsonStream {
135135
stdoutData.append(data)
136136
outContinuation?.yield(data)
137137

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

156-
// Ensures that the process is terminated when the DataChannel is de-referenced.
156+
// Ensures that the process is terminated when the Transport is de-referenced.
157157
let lifetime = Lifetime {
158158
if process.isRunning {
159159
process.terminate()
@@ -177,7 +177,7 @@ extension DataChannel {
177177
throw error
178178
}
179179

180-
let writeHandler: DataChannel.WriteHandler = { [lifetime] data in
180+
let writeHandler: Transport.WriteHandler = { [lifetime] data in
181181
_ = lifetime
182182
if verbose {
183183
logger.log("Sending data:\n\(String(data: data, encoding: .utf8) ?? "nil")")
@@ -188,11 +188,9 @@ extension DataChannel {
188188
stdin.fileHandleForWriting.write(Data("\n".utf8))
189189
}
190190

191-
return DataChannel(writeHandler: writeHandler, dataSequence: outStream)
191+
return Transport(writeHandler: writeHandler, dataSequence: outStream)
192192
}
193193

194-
// MARK: Private
195-
196194
/// Finds the full path to the executable using the `which` command.
197195
private static func locate(executable: String, env: [String: String]? = nil) throws -> String {
198196
let process = Process()
@@ -213,10 +211,12 @@ extension DataChannel {
213211
private static func loadZshEnvironment() throws -> [String: String] {
214212
let process = Process()
215213
process.launchPath = "/bin/zsh"
216-
process.arguments = ["-c", "source ~/.zshrc && printenv"]
214+
// Those are loaded for interactive login shell by zsh:
215+
// https://www.freecodecamp.org/news/how-do-zsh-configuration-files-work/
216+
process.arguments = ["-c", "source ~/.zshenv; source ~/.zprofile; source ~/.zshrc; source ~/.zshrc; printenv"]
217217
let env = try getProcessStdout(process: process)
218218

219-
if let path = env?.split(separator: "\n").filter({ $0.starts(with: "PATH=") }).first {
219+
if let path = env?.split(separator: "\n").filter({ $0.starts(with: "PATH=") }).last {
220220
return ["PATH": String(path.dropFirst("PATH=".count))]
221221
} else {
222222
return ProcessInfo.processInfo.environment
@@ -262,8 +262,6 @@ extension DataChannel {
262262

263263
final class Lifetime {
264264

265-
// MARK: Lifecycle
266-
267265
init(onDeinit: @escaping () -> Void) {
268266
self.onDeinit = onDeinit
269267
}
@@ -272,8 +270,6 @@ final class Lifetime {
272270
onDeinit()
273271
}
274272

275-
// MARK: Private
276-
277273
private let onDeinit: () -> Void
278274

279275
}

MCPClient/Tests/Initialization.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ extension MCPClientTestSuite {
9595
"""),
9696
])
9797

98-
let clientCapabilities = await(client.connection as? MCPClientConnection)?.capabilities
98+
let clientCapabilities = await (client.connection as? MCPClientConnection)?.capabilities
9999
#expect(clientCapabilities?.roots?.listChanged == true)
100100
#expect(clientCapabilities?.sampling != nil)
101101
}

MCPClient/Tests/MCPClientTests.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ class MCPClientTestSuite { }
1212

1313
class MCPClientTest {
1414

15-
// MARK: Lifecycle
16-
1715
init() {
1816
version = "1.0.0"
1917
name = "TestClient"
@@ -42,8 +40,6 @@ class MCPClientTest {
4240
connection.listResourceTemplatesStub = { [] }
4341
}
4442

45-
// MARK: Internal
46-
4743
let version: String
4844
let capabilities: ClientCapabilities
4945
let name: String

MCPClient/Tests/MockMCPClientConnection.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import MCPInterface
77
/// A mock `MCPClientConnection` that can be used in tests.
88
class MockMCPClientConnection: MCPClientConnectionInterface {
99

10-
// MARK: Lifecycle
11-
1210
required init(info: Implementation, capabilities: ClientCapabilities, transport _: Transport = .noop) throws {
1311
self.info = info
1412
self.capabilities = capabilities
@@ -26,8 +24,6 @@ class MockMCPClientConnection: MCPClientConnectionInterface {
2624
self.sendRequestToStream = sendRequestToStream
2725
}
2826

29-
// MARK: Internal
30-
3127
/// Send a server notification.
3228
private(set) var sendNotificationToStream: ((ServerNotification) -> Void) = { _ in }
3329
/// Send a server request to the handler.

0 commit comments

Comments
 (0)