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
9 changes: 7 additions & 2 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ jobs:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Install swiftformat
run: brew install swiftformat
- name: Run linter
run: swift package --allow-writing-to-package-directory format
- 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!)
run: swiftformat --config rules.swiftformat .
- name: Verify that `swiftformat --config rules.swiftformat .` did not change outputs (if it did, please re-run it and re-commit!)
run: git diff --exit-code
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,6 @@ Carthage/Build/
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
fastlane/test_output
.DS_Store
.vscode
24 changes: 8 additions & 16 deletions MCPClient/Sources/MCPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import MCPInterface

public actor MCPClient: MCPClientInterface {

// MARK: Lifecycle

/// Creates a MCP client and connects to the server through the provided transport.
/// The methods completes after connecting to the server.
public init(
Expand All @@ -19,7 +17,7 @@ public actor MCPClient: MCPClientInterface {
async throws {
try await self.init(
capabilities: capabilities,
connection: try MCPClientConnection(
connection: MCPClientConnection(
info: info,
capabilities: ClientCapabilities(
experimental: nil, // TODO: support experimental requests
Expand Down Expand Up @@ -47,31 +45,29 @@ public actor MCPClient: MCPClientInterface {
Task { try await self.updateResourceTemplates() }
}

// MARK: Public

public private(set) var serverInfo: ServerInfo

public var tools: ReadOnlyCurrentValueSubject<CapabilityStatus<[Tool]>, Never> {
get async {
await .init(_tools.compactMap { $0 }.removeDuplicates().eraseToAnyPublisher())
await .init(_tools.compactMap(\.self).removeDuplicates().eraseToAnyPublisher())
}
}

public var prompts: ReadOnlyCurrentValueSubject<CapabilityStatus<[Prompt]>, Never> {
get async {
await .init(_prompts.compactMap { $0 }.removeDuplicates().eraseToAnyPublisher())
await .init(_prompts.compactMap(\.self).removeDuplicates().eraseToAnyPublisher())
}
}

public var resources: ReadOnlyCurrentValueSubject<CapabilityStatus<[Resource]>, Never> {
get async {
await .init(_resources.compactMap { $0 }.removeDuplicates().eraseToAnyPublisher())
await .init(_resources.compactMap(\.self).removeDuplicates().eraseToAnyPublisher())
}
}

public var resourceTemplates: ReadOnlyCurrentValueSubject<CapabilityStatus<[ResourceTemplate]>, Never> {
get async {
await .init(_resourceTemplates.compactMap { $0 }.removeDuplicates().eraseToAnyPublisher())
await .init(_resourceTemplates.compactMap(\.self).removeDuplicates().eraseToAnyPublisher())
}
}

Expand Down Expand Up @@ -99,7 +95,7 @@ public actor MCPClient: MCPClientInterface {
}
// If there has been an error during the execution, throw it
if result.isError == true {
let errors = result.content.compactMap { $0.text }.map { CallToolResult.ExecutionError(text: $0.text) }
let errors = result.content.compactMap(\.text).map { CallToolResult.ExecutionError(text: $0.text) }
throw MCPClientError.toolCallError(executionErrors: errors)
}
return result
Expand All @@ -119,12 +115,8 @@ public actor MCPClient: MCPClientInterface {
return try await connection.readResource(.init(uri: uri))
}

// MARK: Internal

let connection: MCPClientConnectionInterface

// MARK: Private

private let capabilities: ClientCapabilityHandlers

private let _tools = CurrentValueSubject<CapabilityStatus<[Tool]>?, Never>(nil)
Expand Down Expand Up @@ -185,7 +177,7 @@ public actor MCPClient: MCPClientInterface {
private func startListeningToRequests() async {
let requests = await connection.requestsToHandle
Task { [weak self] in
for await(request, completion) in requests {
for await (request, completion) in requests {
guard let self else {
completion(.failure(.init(
code: JRPCErrorCodes.internalError.rawValue,
Expand All @@ -210,7 +202,7 @@ public actor MCPClient: MCPClientInterface {
{
if let handler {
do {
return .success(try await handler(params))
return try await .success(handler(params))
} catch {
return .failure(.init(
code: JRPCErrorCodes.internalError.rawValue,
Expand Down
6 changes: 0 additions & 6 deletions MCPClient/Sources/MCPClientConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import MemberwiseInit

public actor MCPClientConnection: MCPClientConnectionInterface {

// MARK: Lifecycle

public init(
info: Implementation,
capabilities: ClientCapabilities,
Expand All @@ -21,8 +19,6 @@ public actor MCPClientConnection: MCPClientConnectionInterface {
self.capabilities = capabilities
}

// MARK: Public

public let info: Implementation

public let capabilities: ClientCapabilities
Expand Down Expand Up @@ -108,8 +104,6 @@ public actor MCPClientConnection: MCPClientConnectionInterface {
try await jrpcSession.send(RootsListChangedNotification())
}

// MARK: Private

private let _connection: MCPConnection<ServerRequest, ServerNotification>

private var jrpcSession: JSONRPCSession {
Expand Down
4 changes: 2 additions & 2 deletions MCPClient/Sources/MCPClientInterface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ extension MCPClientError: LocalizedError {
public var errorDescription: String? {
switch self {
case .versionMismatch(let received, let expected):
return "Version mismatch between server and client. Received: \(received), Expected: \(expected)"
"Version mismatch between server and client. Received: \(received), Expected: \(expected)"
case .toolCallError(let executionErrors):
return "Error executing tool:\n\(executionErrors.map { $0.errorDescription ?? "unknown error" }.joined(separator: "\n\n"))"
"Error executing tool:\n\(executionErrors.map { $0.errorDescription ?? "unknown error" }.joined(separator: "\n\n"))"
}
}
}
20 changes: 10 additions & 10 deletions MCPClient/Sources/Process+extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ extension Process {
func finish() throws {
/// The full path to the executable + all arguments, each one quoted if it contains a space.
func commandAsString() -> String {
let path: String
if #available(OSX 10.13, *) {
path = self.executableURL?.path ?? ""
} else {
path = launchPath ?? ""
}
let path: String =
if #available(OSX 10.13, *) {
self.executableURL?.path ?? ""
} else {
launchPath ?? ""
}
return (arguments ?? []).reduce(path) { (acc: String, arg: String) in
acc + " " + (arg.contains(" ") ? ("\"" + arg + "\"") : arg)
}
Expand All @@ -66,9 +66,9 @@ enum CommandError: Error, Equatable {
public var errorcode: Int {
switch self {
case .returnedErrorCode(_, let code):
return code
code
case .inAccessibleExecutable:
return 127 // according to http://tldp.org/LDP/abs/html/exitcodes.html
127 // according to http://tldp.org/LDP/abs/html/exitcodes.html
}
}
}
Expand All @@ -79,9 +79,9 @@ extension CommandError: CustomStringConvertible {
public var description: String {
switch self {
case .inAccessibleExecutable(let path):
return "Could not execute file at path '\(path)'."
"Could not execute file at path '\(path)'."
case .returnedErrorCode(let command, let code):
return "Command '\(command)' returned with error code \(code)."
"Command '\(command)' returned with error code \(code)."
}
}
}
20 changes: 6 additions & 14 deletions MCPClient/Sources/stdioTransport/DataChannel+StdioProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,28 @@ extension JSONRPCSetupError: LocalizedError {
public var errorDescription: String? {
switch self {
case .missingStandardIO:
return "Missing standard IO"
"Missing standard IO"
case .couldNotLocateExecutable(let executable, let error):
return "Could not locate executable \(executable) \(error ?? "")".trimmingCharacters(in: .whitespaces)
"Could not locate executable \(executable) \(error ?? "")".trimmingCharacters(in: .whitespaces)
case .standardIOConnectionError(let message):
return "Could not connect to stdio: \(message)".trimmingCharacters(in: .whitespaces)
"Could not connect to stdio: \(message)".trimmingCharacters(in: .whitespaces)
}
}

public var recoverySuggestion: String? {
switch self {
case .missingStandardIO:
return "Make sure that the Process that is passed as an argument has stdin, stdout and stderr set as a Pipe."
"Make sure that the Process that is passed as an argument has stdin, stdout and stderr set as a Pipe."
case .couldNotLocateExecutable:
return "Check that the executable is findable given the PATH environment variable. If needed, pass the right environment to the process."
"Check that the executable is findable given the PATH environment variable. If needed, pass the right environment to the process."
case .standardIOConnectionError:
return nil
nil
}
}
}

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,
Expand Down Expand Up @@ -193,8 +191,6 @@ extension Transport {
return Transport(writeHandler: writeHandler, dataSequence: outStream)
}

// MARK: Private

/// Finds the full path to the executable using the `which` command.
private static func locate(executable: String, env: [String: String]? = nil) throws -> String {
let process = Process()
Expand Down Expand Up @@ -266,8 +262,6 @@ extension Transport {

final class Lifetime {

// MARK: Lifecycle

init(onDeinit: @escaping () -> Void) {
self.onDeinit = onDeinit
}
Expand All @@ -276,8 +270,6 @@ final class Lifetime {
onDeinit()
}

// MARK: Private

private let onDeinit: () -> Void

}
Expand Down
2 changes: 1 addition & 1 deletion MCPClient/Tests/Initialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ extension MCPClientTestSuite {
"""),
])

let clientCapabilities = await(client.connection as? MCPClientConnection)?.capabilities
let clientCapabilities = await (client.connection as? MCPClientConnection)?.capabilities
#expect(clientCapabilities?.roots?.listChanged == true)
#expect(clientCapabilities?.sampling != nil)
}
Expand Down
4 changes: 0 additions & 4 deletions MCPClient/Tests/MCPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ class MCPClientTestSuite { }

class MCPClientTest {

// MARK: Lifecycle

init() {
version = "1.0.0"
name = "TestClient"
Expand Down Expand Up @@ -42,8 +40,6 @@ class MCPClientTest {
connection.listResourceTemplatesStub = { [] }
}

// MARK: Internal

let version: String
let capabilities: ClientCapabilities
let name: String
Expand Down
4 changes: 0 additions & 4 deletions MCPClient/Tests/MockMCPClientConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import MCPInterface
/// A mock `MCPClientConnection` that can be used in tests.
class MockMCPClientConnection: MCPClientConnectionInterface {

// MARK: Lifecycle

required init(info: Implementation, capabilities: ClientCapabilities, transport _: Transport = .noop) throws {
self.info = info
self.capabilities = capabilities
Expand All @@ -26,8 +24,6 @@ class MockMCPClientConnection: MCPClientConnectionInterface {
self.sendRequestToStream = sendRequestToStream
}

// MARK: Internal

/// Send a server notification.
private(set) var sendNotificationToStream: ((ServerNotification) -> Void) = { _ in }
/// Send a server request to the handler.
Expand Down
2 changes: 1 addition & 1 deletion MCPClient/Tests/ServerRequests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extension MCPClientTestSuite {
do {
let success = try response.get()
let roots = try #require(success as? ListRootsResult)
#expect(roots.roots.map { $0.uri } == ["//root"])
#expect(roots.roots.map(\.uri) == ["//root"])
expectation.fulfill()
} catch {
Issue.record(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,15 @@ import MCPInterface
import SwiftAnthropic
import SwiftUI

/// Handle a chat conversation without stream.
@MainActor
@Observable
/// Handle a chat conversation without stream.
final class AnthropicNonStreamManager: ChatManager {

// MARK: Lifecycle

init(service: AnthropicService) {
self.service = service
}

// MARK: Internal

/// Messages sent from the user or received from Claude
var messages = [ChatMessage]()

Expand Down Expand Up @@ -66,8 +62,6 @@ final class AnthropicNonStreamManager: ChatManager {
task = nil
}

// MARK: Private

/// Service to communicate with Anthropic API
private let service: AnthropicService

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,15 @@ import MCPInterface
import SwiftOpenAI
import SwiftUI

/// Handle a chat conversation without stream for OpenAI.
@MainActor
@Observable
/// Handle a chat conversation without stream for OpenAI.
final class OpenAIChatNonStreamManager: ChatManager {

// MARK: Lifecycle

init(service: OpenAIService) {
self.service = service
}

// MARK: Internal

/// Messages sent from the user or received from OpenAI
var messages = [ChatMessage]()

Expand Down Expand Up @@ -66,8 +62,6 @@ final class OpenAIChatNonStreamManager: ChatManager {
task = nil
}

// MARK: Private

/// Service to communicate with OpenAI API
private let service: OpenAIService

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import SwiftUI
/// A view for the user to enter chat messages
struct ChatInputView: View {

// MARK: Internal

/// Is a streaming chat response in progress
let isStreamingResponse: Bool

Expand All @@ -29,8 +27,6 @@ struct ChatInputView: View {
.padding(8)
}

// MARK: Private

private enum FocusedField {
case newMessageText
}
Expand Down
Loading
Loading