Initial commit
This commit is contained in:
commit
cc603d35fe
8 changed files with 970 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
31
Package.swift
Normal file
31
Package.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// swift-tools-version:5.7
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "mcp-swift-sdk",
|
||||
platforms: [
|
||||
.macOS(.v12),
|
||||
.iOS(.v15),
|
||||
.tvOS(.v15),
|
||||
.watchOS(.v8)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||
.library(
|
||||
name: "MCP",
|
||||
targets: ["MCP"]),
|
||||
],
|
||||
dependencies: [],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||
// Targets can depend on other targets in this package and products from dependencies.
|
||||
.target(
|
||||
name: "MCP",
|
||||
dependencies: []),
|
||||
.testTarget(
|
||||
name: "MCPTests",
|
||||
dependencies: ["MCP"]),
|
||||
]
|
||||
)
|
||||
115
README.md
Normal file
115
README.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# MCP Swift SDK
|
||||
|
||||
A Swift implementation of the Model Context Protocol (MCP) client. This SDK provides a modern, Swift-native way to interact with MCP servers.
|
||||
|
||||
## Features
|
||||
|
||||
- Full async/await support
|
||||
- Type-safe API
|
||||
- Resource management
|
||||
- Tool integration
|
||||
- Configurable transport layer
|
||||
- Swift concurrency with actor-based design
|
||||
|
||||
## Requirements
|
||||
|
||||
- Swift 5.7+
|
||||
- macOS 12.0+
|
||||
- iOS 15.0+
|
||||
- tvOS 15.0+
|
||||
- watchOS 8.0+
|
||||
|
||||
## Installation
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
Add the following to your `Package.swift` file:
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/yourusername/mcp-swift-sdk.git", from: "1.0.0")
|
||||
]
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Setup
|
||||
|
||||
```swift
|
||||
import MCP
|
||||
|
||||
// Create a transport (e.g., StdioTransport)
|
||||
let transport = StdioTransport(command: "/path/to/server")
|
||||
|
||||
// Create client info
|
||||
let clientInfo = Implementation(name: "MyApp", version: "1.0.0")
|
||||
|
||||
// Initialize the client
|
||||
let client = MCPClient(transport: transport, clientInfo: clientInfo)
|
||||
|
||||
// Connect and initialize
|
||||
try await client.connect()
|
||||
let result = try await client.initialize()
|
||||
```
|
||||
|
||||
### Working with Resources
|
||||
|
||||
```swift
|
||||
// List available resources
|
||||
let resources = try await client.listResources()
|
||||
|
||||
// Read a resource
|
||||
let resource = try await client.readResource("resource://example")
|
||||
|
||||
// Subscribe to resource updates
|
||||
try await client.subscribeResource("resource://example")
|
||||
|
||||
// Unsubscribe from resource updates
|
||||
try await client.unsubscribeResource("resource://example")
|
||||
```
|
||||
|
||||
### Working with Tools
|
||||
|
||||
```swift
|
||||
// List available tools
|
||||
let tools = try await client.listTools()
|
||||
|
||||
// Call a tool
|
||||
let result = try await client.callTool(name: "example-tool", arguments: ["key": "value"])
|
||||
```
|
||||
|
||||
### Custom Transport
|
||||
|
||||
You can create your own transport by implementing the `MCPTransport` protocol:
|
||||
|
||||
```swift
|
||||
public protocol MCPTransport {
|
||||
func connect() async throws
|
||||
func disconnect() async
|
||||
func sendRequest(_ data: Data) async throws -> Data
|
||||
func sendNotification(_ data: Data) async throws
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The SDK uses the `MCPError` type for error handling:
|
||||
|
||||
```swift
|
||||
public enum MCPError: LocalizedError {
|
||||
case connectionClosed
|
||||
case invalidResponse
|
||||
case invalidParams(String)
|
||||
case serverError(String)
|
||||
case protocolError(String)
|
||||
case transportError(Error)
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
364
Sources/MCP/MCP.swift
Normal file
364
Sources/MCP/MCP.swift
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
import Foundation
|
||||
|
||||
/// Protocol version constants
|
||||
public enum MCPVersion {
|
||||
public static let latest = "1.0.0"
|
||||
public static let supported = ["1.0.0"]
|
||||
}
|
||||
|
||||
/// MCP error types
|
||||
public enum MCPError: LocalizedError {
|
||||
case connectionClosed
|
||||
case invalidResponse
|
||||
case invalidParams(String)
|
||||
case serverError(String)
|
||||
case protocolError(String)
|
||||
case transportError(Error)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .connectionClosed:
|
||||
return "Connection closed"
|
||||
case .invalidResponse:
|
||||
return "Invalid response from server"
|
||||
case .invalidParams(let message):
|
||||
return "Invalid parameters: \(message)"
|
||||
case .serverError(let message):
|
||||
return "Server error: \(message)"
|
||||
case .protocolError(let message):
|
||||
return "Protocol error: \(message)"
|
||||
case .transportError(let error):
|
||||
return "Transport error: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Base protocol for MCP messages
|
||||
public protocol MCPMessage: Codable {
|
||||
var jsonrpc: String { get }
|
||||
}
|
||||
|
||||
/// Protocol for MCP requests
|
||||
public protocol MCPRequest: MCPMessage {
|
||||
var id: String { get }
|
||||
var method: String { get }
|
||||
}
|
||||
|
||||
/// Protocol for MCP notifications
|
||||
public protocol MCPNotification: MCPMessage {
|
||||
var method: String { get }
|
||||
}
|
||||
|
||||
/// Protocol for MCP responses
|
||||
public protocol MCPResponse: MCPMessage {
|
||||
var id: String { get }
|
||||
}
|
||||
|
||||
/// Implementation information
|
||||
public struct Implementation: Codable {
|
||||
public let name: String
|
||||
public let version: String
|
||||
|
||||
public init(name: String, version: String) {
|
||||
self.name = name
|
||||
self.version = version
|
||||
}
|
||||
}
|
||||
|
||||
/// Client capabilities
|
||||
public struct ClientCapabilities: Codable {
|
||||
public var sampling: Bool?
|
||||
public var experimental: [String: [String: Any]]?
|
||||
public var roots: RootsCapability?
|
||||
|
||||
public init(sampling: Bool? = nil,
|
||||
experimental: [String: [String: Any]]? = nil,
|
||||
roots: RootsCapability? = nil) {
|
||||
self.sampling = sampling
|
||||
self.experimental = experimental
|
||||
self.roots = roots
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case sampling
|
||||
case experimental
|
||||
case roots
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
sampling = try container.decodeIfPresent(Bool.self, forKey: .sampling)
|
||||
experimental = try container.decodeIfPresent([String: [String: Any]].self, forKey: .experimental)
|
||||
roots = try container.decodeIfPresent(RootsCapability.self, forKey: .roots)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(sampling, forKey: .sampling)
|
||||
try container.encodeIfPresent(experimental, forKey: .experimental)
|
||||
try container.encodeIfPresent(roots, forKey: .roots)
|
||||
}
|
||||
}
|
||||
|
||||
/// Roots capability configuration
|
||||
public struct RootsCapability: Codable {
|
||||
public var listChanged: Bool?
|
||||
|
||||
public init(listChanged: Bool? = nil) {
|
||||
self.listChanged = listChanged
|
||||
}
|
||||
}
|
||||
|
||||
/// Server capabilities
|
||||
public struct ServerCapabilities: Codable {
|
||||
public var logging: Bool?
|
||||
public var prompts: Bool?
|
||||
public var resources: ResourcesCapability?
|
||||
public var tools: Bool?
|
||||
|
||||
public init(logging: Bool? = nil,
|
||||
prompts: Bool? = nil,
|
||||
resources: ResourcesCapability? = nil,
|
||||
tools: Bool? = nil) {
|
||||
self.logging = logging
|
||||
self.prompts = prompts
|
||||
self.resources = resources
|
||||
self.tools = tools
|
||||
}
|
||||
}
|
||||
|
||||
/// Resources capability configuration
|
||||
public struct ResourcesCapability: Codable {
|
||||
public var subscribe: Bool?
|
||||
|
||||
public init(subscribe: Bool? = nil) {
|
||||
self.subscribe = subscribe
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize request parameters
|
||||
public struct InitializeRequestParams: Codable {
|
||||
public let protocolVersion: String
|
||||
public let capabilities: ClientCapabilities
|
||||
public let clientInfo: Implementation
|
||||
|
||||
public init(protocolVersion: String = MCPVersion.latest,
|
||||
capabilities: ClientCapabilities,
|
||||
clientInfo: Implementation) {
|
||||
self.protocolVersion = protocolVersion
|
||||
self.capabilities = capabilities
|
||||
self.clientInfo = clientInfo
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize request
|
||||
public struct InitializeRequest: MCPRequest {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let method: String = "initialize"
|
||||
public let params: InitializeRequestParams
|
||||
|
||||
public init(id: String = UUID().uuidString, params: InitializeRequestParams) {
|
||||
self.id = id
|
||||
self.params = params
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize result
|
||||
public struct InitializeResult: MCPResponse {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let result: InitializeResultData
|
||||
|
||||
public struct InitializeResultData: Codable {
|
||||
public let protocolVersion: String
|
||||
public let capabilities: ServerCapabilities
|
||||
public let serverInfo: Implementation
|
||||
public let instructions: String?
|
||||
}
|
||||
}
|
||||
|
||||
/// MCP Client protocol
|
||||
public protocol MCPClientProtocol {
|
||||
func connect() async throws
|
||||
func disconnect() async
|
||||
func initialize() async throws -> InitializeResult
|
||||
func ping() async throws
|
||||
|
||||
// Resource methods
|
||||
func listResources() async throws -> ListResourcesResponse
|
||||
func readResource(_ uri: String) async throws -> ReadResourceResponse
|
||||
func subscribeResource(_ uri: String) async throws
|
||||
func unsubscribeResource(_ uri: String) async throws
|
||||
|
||||
// Tool methods
|
||||
func listTools() async throws -> ListToolsResponse
|
||||
func callTool(name: String, arguments: [String: Any]? = nil) async throws -> CallToolResponse
|
||||
}
|
||||
|
||||
/// MCP Client implementation
|
||||
public actor MCPClient: MCPClientProtocol {
|
||||
private let transport: MCPTransport
|
||||
private let clientInfo: Implementation
|
||||
private var capabilities: ClientCapabilities
|
||||
private var serverCapabilities: ServerCapabilities?
|
||||
private var serverVersion: Implementation?
|
||||
private var instructions: String?
|
||||
|
||||
public init(transport: MCPTransport,
|
||||
clientInfo: Implementation,
|
||||
capabilities: ClientCapabilities = ClientCapabilities()) {
|
||||
self.transport = transport
|
||||
self.clientInfo = clientInfo
|
||||
self.capabilities = capabilities
|
||||
}
|
||||
|
||||
public func connect() async throws {
|
||||
try await transport.connect()
|
||||
}
|
||||
|
||||
public func disconnect() async {
|
||||
await transport.disconnect()
|
||||
}
|
||||
|
||||
public func initialize() async throws -> InitializeResult {
|
||||
let request = InitializeRequest(params: InitializeRequestParams(
|
||||
capabilities: capabilities,
|
||||
clientInfo: clientInfo
|
||||
))
|
||||
|
||||
let response: InitializeResult = try await sendRequest(request)
|
||||
|
||||
// Validate protocol version
|
||||
guard MCPVersion.supported.contains(response.result.protocolVersion) else {
|
||||
throw MCPError.protocolError("Unsupported protocol version: \(response.result.protocolVersion)")
|
||||
}
|
||||
|
||||
// Store server information
|
||||
serverCapabilities = response.result.capabilities
|
||||
serverVersion = response.result.serverInfo
|
||||
instructions = response.result.instructions
|
||||
|
||||
// Send initialized notification
|
||||
try await sendNotification(InitializedNotification())
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
public func ping() async throws {
|
||||
let request = PingRequest()
|
||||
try await sendRequest(request)
|
||||
}
|
||||
|
||||
// MARK: - Resource Methods
|
||||
|
||||
public func listResources() async throws -> ListResourcesResponse {
|
||||
guard serverCapabilities?.resources != nil else {
|
||||
throw MCPError.protocolError("Server does not support resources")
|
||||
}
|
||||
|
||||
let request = ListResourcesRequest()
|
||||
return try await sendRequest(request)
|
||||
}
|
||||
|
||||
public func readResource(_ uri: String) async throws -> ReadResourceResponse {
|
||||
guard serverCapabilities?.resources != nil else {
|
||||
throw MCPError.protocolError("Server does not support resources")
|
||||
}
|
||||
|
||||
let request = ReadResourceRequest(uri: uri)
|
||||
return try await sendRequest(request)
|
||||
}
|
||||
|
||||
public func subscribeResource(_ uri: String) async throws {
|
||||
guard let resources = serverCapabilities?.resources,
|
||||
resources.subscribe == true else {
|
||||
throw MCPError.protocolError("Server does not support resource subscriptions")
|
||||
}
|
||||
|
||||
let request = SubscribeRequest(uri: uri)
|
||||
try await sendRequest(request)
|
||||
}
|
||||
|
||||
public func unsubscribeResource(_ uri: String) async throws {
|
||||
guard let resources = serverCapabilities?.resources,
|
||||
resources.subscribe == true else {
|
||||
throw MCPError.protocolError("Server does not support resource subscriptions")
|
||||
}
|
||||
|
||||
let request = UnsubscribeRequest(uri: uri)
|
||||
try await sendRequest(request)
|
||||
}
|
||||
|
||||
// MARK: - Tool Methods
|
||||
|
||||
public func listTools() async throws -> ListToolsResponse {
|
||||
guard serverCapabilities?.tools == true else {
|
||||
throw MCPError.protocolError("Server does not support tools")
|
||||
}
|
||||
|
||||
let request = ListToolsRequest()
|
||||
return try await sendRequest(request)
|
||||
}
|
||||
|
||||
public func callTool(name: String, arguments: [String: Any]? = nil) async throws -> CallToolResponse {
|
||||
guard serverCapabilities?.tools == true else {
|
||||
throw MCPError.protocolError("Server does not support tools")
|
||||
}
|
||||
|
||||
let request = CallToolRequest(name: name, arguments: arguments)
|
||||
return try await sendRequest(request)
|
||||
}
|
||||
|
||||
private func sendRequest<T: MCPRequest, U: MCPResponse>(_ request: T) async throws -> U {
|
||||
let data = try JSONEncoder().encode(request)
|
||||
let responseData = try await transport.sendRequest(data)
|
||||
return try JSONDecoder().decode(U.self, from: responseData)
|
||||
}
|
||||
|
||||
private func sendNotification<T: MCPNotification>(_ notification: T) async throws {
|
||||
let data = try JSONEncoder().encode(notification)
|
||||
try await transport.sendNotification(data)
|
||||
}
|
||||
|
||||
// MARK: - Public Properties
|
||||
|
||||
/// Get the server's capabilities after initialization
|
||||
public var serverCapabilitiesAfterInit: ServerCapabilities? {
|
||||
serverCapabilities
|
||||
}
|
||||
|
||||
/// Get the server's version information after initialization
|
||||
public var serverVersionAfterInit: Implementation? {
|
||||
serverVersion
|
||||
}
|
||||
|
||||
/// Get the server's instructions after initialization
|
||||
public var serverInstructions: String? {
|
||||
instructions
|
||||
}
|
||||
}
|
||||
|
||||
/// MCP Transport protocol
|
||||
public protocol MCPTransport {
|
||||
func connect() async throws
|
||||
func disconnect() async
|
||||
func sendRequest(_ data: Data) async throws -> Data
|
||||
func sendNotification(_ data: Data) async throws
|
||||
}
|
||||
|
||||
/// Basic notification types
|
||||
public struct InitializedNotification: MCPNotification {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let method: String = "notifications/initialized"
|
||||
}
|
||||
|
||||
public struct PingRequest: MCPRequest {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let method: String = "ping"
|
||||
|
||||
public init(id: String = UUID().uuidString) {
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
166
Sources/MCP/Resources.swift
Normal file
166
Sources/MCP/Resources.swift
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import Foundation
|
||||
|
||||
/// Resource metadata
|
||||
public struct ResourceMetadata: Codable {
|
||||
public var name: String
|
||||
public var uri: String
|
||||
public var metadata: [String: String]?
|
||||
|
||||
public init(name: String, uri: String, metadata: [String: String]? = nil) {
|
||||
self.name = name
|
||||
self.uri = uri
|
||||
self.metadata = metadata
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource content types
|
||||
public enum ResourceContent: Codable {
|
||||
case text(String)
|
||||
case binary(Data)
|
||||
case embedded([EmbeddedResource])
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case content
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
|
||||
switch type {
|
||||
case "text":
|
||||
let content = try container.decode(String.self, forKey: .content)
|
||||
self = .text(content)
|
||||
case "binary":
|
||||
let content = try container.decode(Data.self, forKey: .content)
|
||||
self = .binary(content)
|
||||
case "embedded":
|
||||
let content = try container.decode([EmbeddedResource].self, forKey: .content)
|
||||
self = .embedded(content)
|
||||
default:
|
||||
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown resource type")
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
switch self {
|
||||
case .text(let content):
|
||||
try container.encode("text", forKey: .type)
|
||||
try container.encode(content, forKey: .content)
|
||||
case .binary(let content):
|
||||
try container.encode("binary", forKey: .type)
|
||||
try container.encode(content, forKey: .content)
|
||||
case .embedded(let content):
|
||||
try container.encode("embedded", forKey: .type)
|
||||
try container.encode(content, forKey: .content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Embedded resource type
|
||||
public struct EmbeddedResource: Codable {
|
||||
public var type: String
|
||||
public var data: String
|
||||
public var metadata: [String: String]?
|
||||
|
||||
public init(type: String, data: String, metadata: [String: String]? = nil) {
|
||||
self.type = type
|
||||
self.data = data
|
||||
self.metadata = metadata
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource request types
|
||||
public struct ListResourcesRequest: MCPRequest {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let method: String = "resources/list"
|
||||
|
||||
public init(id: String = UUID().uuidString) {
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
|
||||
public struct ReadResourceRequest: MCPRequest {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let method: String = "resources/read"
|
||||
public let params: ReadResourceParams
|
||||
|
||||
public struct ReadResourceParams: Codable {
|
||||
public let uri: String
|
||||
}
|
||||
|
||||
public init(id: String = UUID().uuidString, uri: String) {
|
||||
self.id = id
|
||||
self.params = ReadResourceParams(uri: uri)
|
||||
}
|
||||
}
|
||||
|
||||
public struct SubscribeRequest: MCPRequest {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let method: String = "resources/subscribe"
|
||||
public let params: SubscribeParams
|
||||
|
||||
public struct SubscribeParams: Codable {
|
||||
public let uri: String
|
||||
}
|
||||
|
||||
public init(id: String = UUID().uuidString, uri: String) {
|
||||
self.id = id
|
||||
self.params = SubscribeParams(uri: uri)
|
||||
}
|
||||
}
|
||||
|
||||
public struct UnsubscribeRequest: MCPRequest {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let method: String = "resources/unsubscribe"
|
||||
public let params: UnsubscribeParams
|
||||
|
||||
public struct UnsubscribeParams: Codable {
|
||||
public let uri: String
|
||||
}
|
||||
|
||||
public init(id: String = UUID().uuidString, uri: String) {
|
||||
self.id = id
|
||||
self.params = UnsubscribeParams(uri: uri)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource response types
|
||||
public struct ListResourcesResponse: MCPResponse {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let result: ListResourcesResult
|
||||
|
||||
public struct ListResourcesResult: Codable {
|
||||
public let resources: [ResourceMetadata]
|
||||
}
|
||||
}
|
||||
|
||||
public struct ReadResourceResponse: MCPResponse {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let result: ReadResourceResult
|
||||
|
||||
public struct ReadResourceResult: Codable {
|
||||
public let content: ResourceContent
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource notification types
|
||||
public struct ResourceUpdatedNotification: MCPNotification {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let method: String = "notifications/resource/updated"
|
||||
public let params: ResourceUpdatedParams
|
||||
|
||||
public struct ResourceUpdatedParams: Codable {
|
||||
public let uri: String
|
||||
public let content: ResourceContent
|
||||
}
|
||||
}
|
||||
105
Sources/MCP/StdioTransport.swift
Normal file
105
Sources/MCP/StdioTransport.swift
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import Foundation
|
||||
|
||||
/// A transport implementation that communicates over standard input/output
|
||||
public actor StdioTransport: MCPTransport {
|
||||
private var isConnected = false
|
||||
private let processQueue = DispatchQueue(label: "com.mcp.stdio", qos: .userInitiated)
|
||||
private let process: Process
|
||||
private let inputPipe = Pipe()
|
||||
private let outputPipe = Pipe()
|
||||
private let errorPipe = Pipe()
|
||||
|
||||
public init(command: String, arguments: [String] = [], environment: [String: String]? = nil) {
|
||||
self.process = Process()
|
||||
self.process.executableURL = URL(fileURLWithPath: command)
|
||||
self.process.arguments = arguments
|
||||
if let env = environment {
|
||||
self.process.environment = env
|
||||
}
|
||||
|
||||
self.process.standardInput = inputPipe
|
||||
self.process.standardOutput = outputPipe
|
||||
self.process.standardError = errorPipe
|
||||
}
|
||||
|
||||
public func connect() async throws {
|
||||
guard !isConnected else { return }
|
||||
|
||||
try process.run()
|
||||
isConnected = true
|
||||
|
||||
// Start reading output asynchronously
|
||||
Task {
|
||||
await readOutput()
|
||||
}
|
||||
}
|
||||
|
||||
public func disconnect() async {
|
||||
guard isConnected else { return }
|
||||
|
||||
process.terminate()
|
||||
isConnected = false
|
||||
}
|
||||
|
||||
public func sendRequest(_ data: Data) async throws -> Data {
|
||||
guard isConnected else {
|
||||
throw MCPError.connectionClosed
|
||||
}
|
||||
|
||||
// Write request
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
processQueue.async {
|
||||
do {
|
||||
let message = data + "\n".data(using: .utf8)!
|
||||
try self.inputPipe.fileHandleForWriting.write(contentsOf: message)
|
||||
|
||||
// Read response
|
||||
if let response = try self.outputPipe.fileHandleForReading.readToEnd() {
|
||||
continuation.resume(returning: response)
|
||||
} else {
|
||||
continuation.resume(throwing: MCPError.invalidResponse)
|
||||
}
|
||||
} catch {
|
||||
continuation.resume(throwing: MCPError.transportError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func sendNotification(_ data: Data) async throws {
|
||||
guard isConnected else {
|
||||
throw MCPError.connectionClosed
|
||||
}
|
||||
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
processQueue.async {
|
||||
do {
|
||||
let message = data + "\n".data(using: .utf8)!
|
||||
try self.inputPipe.fileHandleForWriting.write(contentsOf: message)
|
||||
continuation.resume()
|
||||
} catch {
|
||||
continuation.resume(throwing: MCPError.transportError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func readOutput() async {
|
||||
let handle = outputPipe.fileHandleForReading
|
||||
|
||||
while isConnected {
|
||||
do {
|
||||
let data = try handle.read(upToCount: 4096)
|
||||
if let data = data, !data.isEmpty {
|
||||
// Process received data
|
||||
// In a real implementation, you would parse the JSON-RPC messages
|
||||
// and handle them appropriately
|
||||
print("Received data: \(String(data: data, encoding: .utf8) ?? "")")
|
||||
}
|
||||
} catch {
|
||||
print("Error reading output: \(error)")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
175
Sources/MCP/Tools.swift
Normal file
175
Sources/MCP/Tools.swift
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import Foundation
|
||||
|
||||
/// Tool metadata
|
||||
public struct Tool: Codable {
|
||||
public let name: String
|
||||
public let description: String?
|
||||
public let inputSchema: [String: Any]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case description
|
||||
case inputSchema
|
||||
}
|
||||
|
||||
public init(name: String, description: String? = nil, inputSchema: [String: Any]? = nil) {
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.inputSchema = inputSchema
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
description = try container.decodeIfPresent(String.self, forKey: .description)
|
||||
inputSchema = try container.decodeIfPresent([String: Any].self, forKey: .inputSchema)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encodeIfPresent(description, forKey: .description)
|
||||
if let schema = inputSchema {
|
||||
try container.encode(schema, forKey: .inputSchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool request types
|
||||
public struct ListToolsRequest: MCPRequest {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let method: String = "tools/list"
|
||||
|
||||
public init(id: String = UUID().uuidString) {
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
|
||||
public struct CallToolRequest: MCPRequest {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let method: String = "tools/call"
|
||||
public let params: CallToolParams
|
||||
|
||||
public struct CallToolParams: Codable {
|
||||
public let name: String
|
||||
public let arguments: [String: Any]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case arguments
|
||||
}
|
||||
|
||||
public init(name: String, arguments: [String: Any]? = nil) {
|
||||
self.name = name
|
||||
self.arguments = arguments
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
arguments = try container.decodeIfPresent([String: Any].self, forKey: .arguments)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(name, forKey: .name)
|
||||
if let args = arguments {
|
||||
try container.encode(args, forKey: .arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init(id: String = UUID().uuidString, name: String, arguments: [String: Any]? = nil) {
|
||||
self.id = id
|
||||
self.params = CallToolParams(name: name, arguments: arguments)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool response types
|
||||
public struct ListToolsResponse: MCPResponse {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let result: ListToolsResult
|
||||
|
||||
public struct ListToolsResult: Codable {
|
||||
public let tools: [Tool]
|
||||
}
|
||||
}
|
||||
|
||||
public struct CallToolResponse: MCPResponse {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let id: String
|
||||
public let result: CallToolResult
|
||||
|
||||
public struct CallToolResult: Codable {
|
||||
public let content: [ToolContent]
|
||||
public let isError: Bool?
|
||||
|
||||
public init(content: [ToolContent], isError: Bool? = nil) {
|
||||
self.content = content
|
||||
self.isError = isError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool content types
|
||||
public enum ToolContent: Codable {
|
||||
case text(String)
|
||||
case image(ImageContent)
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case text
|
||||
case image
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
|
||||
switch type {
|
||||
case "text":
|
||||
let text = try container.decode(String.self, forKey: .text)
|
||||
self = .text(text)
|
||||
case "image":
|
||||
let image = try container.decode(ImageContent.self, forKey: .image)
|
||||
self = .image(image)
|
||||
default:
|
||||
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown tool content type")
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
switch self {
|
||||
case .text(let text):
|
||||
try container.encode("text", forKey: .type)
|
||||
try container.encode(text, forKey: .text)
|
||||
case .image(let image):
|
||||
try container.encode("image", forKey: .type)
|
||||
try container.encode(image, forKey: .image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Image content type
|
||||
public struct ImageContent: Codable {
|
||||
public let data: String
|
||||
public let mimeType: String
|
||||
public let metadata: [String: String]?
|
||||
|
||||
public init(data: String, mimeType: String, metadata: [String: String]? = nil) {
|
||||
self.data = data
|
||||
self.mimeType = mimeType
|
||||
self.metadata = metadata
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool notification types
|
||||
public struct ToolListChangedNotification: MCPNotification {
|
||||
public let jsonrpc: String = "2.0"
|
||||
public let method: String = "notifications/tools/list_changed"
|
||||
}
|
||||
6
Tests/MCPTests/MCPTests.swift
Normal file
6
Tests/MCPTests/MCPTests.swift
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import Testing
|
||||
@testable import MCP
|
||||
|
||||
@Test func example() async throws {
|
||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||
}
|
||||
Loading…
Reference in a new issue