├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .spi.yml ├── .vscode └── launch.json ├── Demos └── SwiftMCPDemo │ ├── Commands │ ├── HTTPSSECommand.swift │ └── StdioCommand.swift │ ├── DemoError.swift │ ├── DemoServer+MCPResourceProviding.swift │ ├── DemoServer.swift │ ├── FileResource.swift │ ├── Logging │ ├── LoggingSystem.swift │ └── OSLogHandler.swift │ ├── MCPCommand.swift │ ├── SignalHandler.swift │ └── String+MIME.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── SwiftMCP │ ├── Errors │ │ └── MCPResourceError.swift │ ├── Extensions │ │ ├── Array+CaseIterableElements.swift │ │ ├── Array+CaseLabels.swift │ │ ├── Array+MCPPrompt.swift │ │ ├── Array+MCPTool.swift │ │ ├── Array+SchemaRepresentableElements.swift │ │ ├── BinaryFloatingPoint+Conversion.swift │ │ ├── BinaryInteger+Conversion.swift │ │ ├── BinaryNumeric+Convert.swift │ │ ├── Dictionary+ParameterExtraction.swift │ │ ├── JSON+DateEncoding.swift │ │ ├── JSONRPCMessage+Batch.swift │ │ ├── MCPParameterInfo+Completion.swift │ │ ├── MCPParameterInfo+JSONSchema.swift │ │ ├── MCPServer+Batch.swift │ │ ├── MCPToolMetadata+Arguments.swift │ │ ├── String+CompletionSorting.swift │ │ ├── String+ContentType.swift │ │ ├── String+Hostname.swift │ │ ├── String+OpenAPI.swift │ │ ├── String+Quotes.swift │ │ ├── Type+JSONSchema.swift │ │ └── URL+TemplateExtraction.swift │ ├── MCPMacros.swift │ ├── Models │ │ ├── Completion │ │ │ ├── CompleteRequest.swift │ │ │ └── CompleteResult.swift │ │ ├── Initialization │ │ │ └── ServerCapabilities.swift │ │ ├── InitializeResult.swift │ │ ├── JSONRPC │ │ │ └── JSONRPCMessage.swift │ │ ├── JSONSchema+Codable.swift │ │ ├── JSONSchema.swift │ │ ├── MCPFunctionMetadata.swift │ │ ├── MCPParameterInfo.swift │ │ ├── Prompts │ │ │ ├── MCPPromptMetadata.swift │ │ │ ├── Prompt.swift │ │ │ └── PromptMessage.swift │ │ ├── Resources │ │ │ ├── GenericResourceContent.swift │ │ │ ├── MCPResource.swift │ │ │ ├── MCPResourceContent.swift │ │ │ ├── MCPResourceKind.swift │ │ │ ├── MCPResourceMetadata.swift │ │ │ └── MCPResourceTemplate.swift │ │ ├── Schema │ │ │ ├── SchemaMetadata.swift │ │ │ ├── SchemaPropertyInfo.swift │ │ │ └── SchemaRepresentable.swift │ │ └── Tools │ │ │ ├── MCPTool.swift │ │ │ ├── MCPToolError.swift │ │ │ └── MCPToolMetadata.swift │ ├── OpenAPI │ │ ├── AIPluginManifest.swift │ │ ├── FileContent.swift │ │ ├── OpenAIFileResponse.swift │ │ └── OpenAPISpec.swift │ ├── Protocols │ │ ├── MCPCompletionProviding.swift │ │ ├── MCPPromptProviding.swift │ │ ├── MCPResourceProviding.swift │ │ ├── MCPServer.swift │ │ ├── MCPService.swift │ │ └── MCPToolProviding.swift │ ├── SwiftMCP.docc │ │ ├── Articles │ │ │ ├── CoreConcepts.md │ │ │ ├── GettingStarted.md │ │ │ └── SupportedTypes.md │ │ ├── Resources │ │ │ ├── 01-calculator-base.swift │ │ │ ├── 02-calculator-server.swift │ │ │ ├── 03-calculator-tool.swift │ │ │ ├── 04-calculator-named.swift │ │ │ ├── 04-greeting-error.swift │ │ │ ├── 05-calculator-description.swift │ │ │ ├── 05-calculator-throwing.swift │ │ │ ├── 06-calculator-async.swift │ │ │ ├── 06-calculator-schema.swift │ │ │ └── placeholder.png │ │ ├── SwiftMCP.md │ │ └── Tutorials │ │ │ ├── BuildingAnMCPServer.tutorial │ │ │ └── SwiftMCPTutorials.tutorial │ ├── SwiftMCP.swift │ ├── Transport │ │ ├── Channel+SSE.swift │ │ ├── HTTPHandler.swift │ │ ├── HTTPLogger.swift │ │ ├── HTTPSSETransport.swift │ │ ├── RequestState.swift │ │ ├── SSEChannelManager.swift │ │ ├── SSEMessage.swift │ │ ├── StdioTransport.swift │ │ ├── Transport.swift │ │ └── TransportError.swift │ └── Util │ │ └── DictionaryEncoder.swift └── SwiftMCPMacros │ ├── Documentation.swift │ ├── FunctionMetadataExtractor.swift │ ├── MCPDiagnostics.swift │ ├── MCPPromptMacro.swift │ ├── MCPResourceDiagnostics.swift │ ├── MCPResourceMacro.swift │ ├── MCPServerDiagnostics.swift │ ├── MCPServerMacro.swift │ ├── MCPToolMacro.swift │ ├── SchemaMacro.swift │ ├── String+Documentation.swift │ ├── SwiftMCPPlugin.swift │ └── URITemplateValidator.swift └── Tests └── SwiftMCPTests ├── Calculator.swift ├── CalculatorMockTests.swift ├── CalculatorTests.swift ├── CompletionTests.swift ├── ComplexTypesTests.swift ├── DemoError.swift ├── DictionaryEncoderTests.swift ├── EnumTests.swift ├── Extensions ├── Array+CaseLabelsTests.swift └── StringContentTypeTests.swift ├── JSONRPCRequestTest.swift ├── MCPResourceDemoTests.swift ├── MCPResourceTests.swift ├── MCPServerAutoConformanceTests.swift ├── MCPServerParametersTests.swift ├── MCPServerTests.swift ├── MCPToolArgumentEnrichingTests.swift ├── MCPToolDefaultValueTests.swift ├── MCPToolTests.swift ├── MCPToolWarningTests.swift ├── MacroDocumentationTests.swift ├── MockClient.swift ├── NoOpLogHandler.swift ├── OpenAPIResourceTests.swift ├── OpenAPISpecTests.swift ├── PingTests.swift ├── PromptTests.swift ├── RFC6570ExtractionTests.swift ├── TestError.swift ├── URIConstructionTests.swift └── URITemplateValidatorTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | jobs: 10 | build-macos: 11 | runs-on: macos-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup Swift 6.0 17 | uses: swift-actions/setup-swift@v2.2.0 18 | with: 19 | swift-version: "6.0" 20 | 21 | - name: Verify Swift version 22 | run: swift --version 23 | 24 | - name: Build & Test (macOS) 25 | run: swift test 26 | 27 | build-linux: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: Setup Swift 6.0 34 | uses: swift-actions/setup-swift@v2.2.0 35 | with: 36 | swift-version: "6.0" 37 | 38 | - name: Verify Swift version 39 | run: swift --version 40 | 41 | - name: Build & Test (Linux) 42 | run: swift test 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | # Environment variables 10 | .env 11 | .env.* 12 | !.env.example 13 | Package.resolved 14 | .swiftpm 15 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [SwiftMCP] 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "swift", 5 | "request": "launch", 6 | "args": [], 7 | "cwd": "${workspaceFolder:SwiftMCP}", 8 | "name": "Debug SwiftMCPDemo", 9 | "program": "${workspaceFolder:SwiftMCP}/.build/debug/SwiftMCPDemo", 10 | "preLaunchTask": "swift: Build Debug SwiftMCPDemo" 11 | }, 12 | { 13 | "type": "swift", 14 | "request": "launch", 15 | "args": [], 16 | "cwd": "${workspaceFolder:SwiftMCP}", 17 | "name": "Release SwiftMCPDemo", 18 | "program": "${workspaceFolder:SwiftMCP}/.build/release/SwiftMCPDemo", 19 | "preLaunchTask": "swift: Build Release SwiftMCPDemo" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /Demos/SwiftMCPDemo/Commands/HTTPSSECommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ArgumentParser 3 | import SwiftMCP 4 | import Logging 5 | #if canImport(OSLog) 6 | import OSLog 7 | #endif 8 | 9 | 10 | /** 11 | A command that starts an HTTP server with Server-Sent Events (SSE) support for SwiftMCP. 12 | 13 | This mode provides a long-running HTTP server that supports: 14 | - Server-Sent Events (SSE) for real-time updates 15 | - JSON-RPC over HTTP POST for function calls 16 | - Optional bearer token authentication 17 | - Optional OpenAPI endpoints for AI plugin integration 18 | 19 | Key Features: 20 | 1. Server-Sent Events: 21 | - Connect to `/sse` endpoint for real-time updates 22 | - Receive function call results and notifications 23 | - Maintain persistent connections with clients 24 | 25 | 2. JSON-RPC Endpoints: 26 | - Send POST requests to `//` 27 | - Standard JSON-RPC 2.0 request/response format 28 | - Support for batched requests 29 | 30 | 3. Security: 31 | - Optional bearer token authentication 32 | - CORS support for web clients 33 | - Secure error handling and validation 34 | 35 | 4. AI Plugin Support: 36 | - OpenAPI specification at `/openapi.json` 37 | - AI plugin manifest at `/.well-known/ai-plugin.json` 38 | - Compatible with AI plugin standards 39 | */ 40 | final class HTTPSSECommand: AsyncParsableCommand { 41 | static let configuration = CommandConfiguration( 42 | commandName: "httpsse", 43 | abstract: "Start an HTTP server with Server-Sent Events (SSE) support", 44 | discussion: """ 45 | Start an HTTP server that supports Server-Sent Events (SSE) and JSON-RPC. 46 | 47 | Features: 48 | - Server-Sent Events endpoint at /sse 49 | - JSON-RPC endpoints at // 50 | - Optional bearer token authentication 51 | - Optional OpenAPI endpoints for AI plugin integration 52 | 53 | Examples: 54 | # Basic usage 55 | SwiftMCPDemo httpsse --port 8080 56 | 57 | # With authentication 58 | SwiftMCPDemo httpsse --port 8080 --token my-secret-token 59 | 60 | # With OpenAPI support 61 | SwiftMCPDemo httpsse --port 8080 --openapi 62 | """ 63 | ) 64 | 65 | @Option(name: .long, help: "The port to listen on") 66 | var port: Int 67 | 68 | @Option(name: .long, help: "Bearer token for authorization") 69 | var token: String? 70 | 71 | @Flag(name: .long, help: "Enable OpenAPI endpoints") 72 | var openapi: Bool = false 73 | 74 | // Make this a computed property instead of stored property 75 | private var signalHandler: SignalHandler? = nil 76 | 77 | required init() {} 78 | 79 | // Add manual Decodable conformance 80 | required init(from decoder: Decoder) throws { 81 | let container = try decoder.container(keyedBy: CodingKeys.self) 82 | self.port = try container.decode(Int.self, forKey: .port) 83 | self.token = try container.decodeIfPresent(String.self, forKey: .token) 84 | self.openapi = try container.decode(Bool.self, forKey: .openapi) 85 | } 86 | 87 | private enum CodingKeys: String, CodingKey { 88 | case port 89 | case token 90 | case openapi 91 | } 92 | 93 | func run() async throws { 94 | #if canImport(OSLog) 95 | LoggingSystem.bootstrapWithOSLog() 96 | #endif 97 | 98 | let calculator = DemoServer() 99 | 100 | let host = String.localHostname 101 | print("MCP Server \(calculator.serverName) (\(calculator.serverVersion)) started with HTTP+SSE transport on http://\(host):\(port)/sse") 102 | 103 | let transport = HTTPSSETransport(server: calculator, port: port) 104 | 105 | // Set up authorization handler if token is provided 106 | if let requiredToken = token { 107 | transport.authorizationHandler = { token in 108 | guard let token else { 109 | return .unauthorized("Missing bearer token") 110 | } 111 | 112 | guard token == requiredToken else { 113 | return .unauthorized("Invalid bearer token") 114 | } 115 | 116 | return .authorized 117 | } 118 | } 119 | 120 | // Enable OpenAPI endpoints if requested 121 | transport.serveOpenAPI = openapi 122 | 123 | // Set up signal handling to shut down the transport on Ctrl+C 124 | signalHandler = SignalHandler(transport: transport) 125 | await signalHandler?.setup() 126 | 127 | // Run the server (blocking) 128 | try await transport.run() 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Demos/SwiftMCPDemo/Commands/StdioCommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ArgumentParser 3 | import SwiftMCP 4 | import Logging 5 | import NIOCore 6 | #if canImport(OSLog) 7 | import OSLog 8 | #endif 9 | 10 | /** 11 | A command that processes JSON-RPC requests from standard input and writes responses to standard output. 12 | 13 | This is the default mode of operation for the SwiftMCP demo. It's designed to: 14 | - Read JSON-RPC requests line by line from stdin 15 | - Process each request using the configured MCP server 16 | - Write JSON-RPC responses to stdout 17 | - Log status messages to stderr to avoid interfering with the JSON-RPC protocol 18 | 19 | This mode is particularly useful for: 20 | - Integration with other tools via Unix pipes 21 | - Testing and debugging MCP functions 22 | - Scripting and automation 23 | */ 24 | struct StdioCommand: AsyncParsableCommand { 25 | static let configuration = CommandConfiguration( 26 | commandName: "stdio", 27 | abstract: "Read JSON-RPC requests from stdin and write responses to stdout", 28 | discussion: """ 29 | Read JSON-RPC requests from stdin and write responses to stdout. 30 | 31 | This mode is perfect for integration with other tools via pipes. 32 | 33 | Examples: 34 | # Basic usage 35 | SwiftMCPDemo stdio 36 | 37 | # With pipe 38 | echo '{"jsonrpc": "2.0", "method": "add", "params": [1, 2]}' | SwiftMCPDemo stdio 39 | """ 40 | ) 41 | 42 | func run() async throws { 43 | #if canImport(OSLog) 44 | LoggingSystem.bootstrapWithOSLog() 45 | #endif 46 | 47 | let calculator = DemoServer() 48 | 49 | do { 50 | // need to output to stderror or else npx complains 51 | logToStderr("MCP Server \(calculator.serverName) (\(calculator.serverVersion)) started with Stdio transport") 52 | 53 | let transport = StdioTransport(server: calculator) 54 | try await transport.run() 55 | } 56 | catch let error as TransportError { 57 | // Handle transport errors 58 | let errorMessage = """ 59 | Transport Error: \(error.localizedDescription) 60 | """ 61 | logToStderr(errorMessage) 62 | Foundation.exit(1) 63 | } 64 | catch let error as ChannelError { 65 | // Handle specific channel errors 66 | logToStderr("Channel Error: \(error)") 67 | Foundation.exit(1) 68 | } 69 | catch { 70 | // Handle any other errors 71 | logToStderr("Error: \(error)") 72 | Foundation.exit(1) 73 | } 74 | } 75 | } 76 | 77 | /// Function to log a message to stderr 78 | func logToStderr(_ message: String) { 79 | guard let data = (message + "\n").data(using: .utf8) else { return } 80 | try? FileHandle.standardError.write(contentsOf: data) 81 | } 82 | 83 | -------------------------------------------------------------------------------- /Demos/SwiftMCPDemo/DemoError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Custom errors for the demo app 4 | public enum DemoError: LocalizedError { 5 | /// When a greeting name is too short 6 | case nameTooShort(name: String) 7 | 8 | /// When a greeting name contains invalid characters 9 | case invalidName(name: String) 10 | 11 | /// When the greeting service is temporarily unavailable 12 | case serviceUnavailable 13 | 14 | public var errorDescription: String? { 15 | switch self { 16 | case .nameTooShort(let name): 17 | return "Name '\(name)' is too short. Names must be at least 2 characters long." 18 | case .invalidName(let name): 19 | return "Name '\(name)' contains invalid characters. Only letters and spaces are allowed." 20 | case .serviceUnavailable: 21 | return "The greeting service is temporarily unavailable. Please try again later." 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Demos/SwiftMCPDemo/DemoServer+MCPResourceProviding.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// DemoServer+MCPResourceProviding.swift 3 | //// SwiftMCP 4 | //// 5 | //// Created by Oliver Drobnik on 03.04.25. 6 | //// 7 | 8 | import Foundation 9 | import SwiftMCP 10 | 11 | extension DemoServer 12 | { 13 | /// Returns an array of all MCP resources defined in this type 14 | var mcpResources: [any MCPResource] { 15 | get async { 16 | return await getDynamicFileResources() 17 | } 18 | } 19 | 20 | /// Returns dynamic file-based resources from Downloads folder 21 | private func getDynamicFileResources() async -> [MCPResource] { 22 | // Get the Downloads folder URL 23 | guard let downloadsURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else { 24 | logToStderr("Could not get Downloads folder URL") 25 | return [] 26 | } 27 | 28 | do { 29 | // List all files in the Downloads folder 30 | let fileURLs = try FileManager.default.contentsOfDirectory( 31 | at: downloadsURL, 32 | includingPropertiesForKeys: [.isRegularFileKey, .nameKey, .fileSizeKey], 33 | options: [.skipsHiddenFiles] 34 | ) 35 | 36 | // Filter to only include regular files 37 | let regularFileURLs = fileURLs.filter { url in 38 | do { 39 | let resourceValues = try url.resourceValues(forKeys: [.isRegularFileKey]) 40 | return resourceValues.isRegularFile ?? false 41 | } catch { 42 | return false 43 | } 44 | } 45 | 46 | // Create FileResource objects for each file 47 | return regularFileURLs.map { fileURL in 48 | // Get file attributes for description 49 | let fileAttributes: String 50 | do { 51 | let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path) 52 | let fileSize = attributes[.size] as? Int64 ?? 0 53 | let modificationDate = attributes[.modificationDate] as? Date ?? Date() 54 | let formatter = DateFormatter() 55 | formatter.dateStyle = .medium 56 | formatter.timeStyle = .short 57 | fileAttributes = "Size: \(ByteCountFormatter.string(fromByteCount: fileSize, countStyle: .file)), Modified: \(formatter.string(from: modificationDate))" 58 | } catch { 59 | fileAttributes = "File in Downloads folder" 60 | } 61 | 62 | return FileResource( 63 | uri: fileURL, 64 | name: fileURL.lastPathComponent, 65 | description: fileAttributes 66 | ) 67 | } 68 | } catch { 69 | logToStderr("Error listing files in Downloads folder: \(error.localizedDescription)") 70 | return [] 71 | } 72 | } 73 | 74 | /// Override to handle file-based resources by reading actual file content 75 | public func getNonTemplateResource(uri: URL) async throws -> [MCPResourceContent] { 76 | // Check if the file exists 77 | guard FileManager.default.fileExists(atPath: uri.path) else { 78 | return [] 79 | } 80 | 81 | // Get the resource content 82 | return try [FileResourceContent.from(fileURL: uri)] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Demos/SwiftMCPDemo/FileResource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftMCP 3 | 4 | /// A resource implementation for files in the file system 5 | @Schema 6 | public struct FileResource: MCPResource { 7 | /// The URI of the resource 8 | public let uri: URL 9 | 10 | /// The name of the resource 11 | public let name: String 12 | 13 | /// The description of the resource 14 | public let description: String 15 | 16 | /// The MIME type of the resource 17 | public let mimeType: String 18 | 19 | /// Creates a new FileResource 20 | /// - Parameters: 21 | /// - uri: The URI of the file 22 | /// - name: The name of the resource (defaults to the filename) 23 | /// - description: The description of the resource (defaults to the file path) 24 | /// - mimeType: The MIME type of the resource (defaults to a guess based on file extension) 25 | public init(uri: URL, name: String? = nil, description: String? = nil, mimeType: String? = nil) { 26 | self.uri = uri 27 | self.name = name ?? uri.lastPathComponent 28 | self.description = description ?? "File at \(uri.path)" 29 | 30 | if let mimeType = mimeType { 31 | self.mimeType = mimeType 32 | } else { 33 | // Try to determine MIME type from file extension 34 | let fileExtension = uri.pathExtension 35 | self.mimeType = String.mimeType(for: fileExtension) 36 | } 37 | } 38 | } 39 | 40 | /// A resource content implementation for files in the file system 41 | @Schema 42 | public struct FileResourceContent: MCPResourceContent { 43 | /// The URI of the resource 44 | public let uri: URL 45 | 46 | /// The MIME type of the resource 47 | public let mimeType: String? 48 | 49 | /// The text content of the resource (if it's a text resource) 50 | public let text: String? 51 | 52 | /// The binary content of the resource (if it's a binary resource) 53 | public let blob: Data? 54 | 55 | /// Creates a new FileResourceContent 56 | /// - Parameters: 57 | /// - uri: The URI of the file 58 | /// - mimeType: The MIME type of the resource (optional) 59 | /// - text: The text content of the resource (if it's a text resource) 60 | /// - blob: The binary content of the resource (if it's a binary resource) 61 | public init(uri: URL, mimeType: String? = nil, text: String? = nil, blob: Data? = nil) { 62 | self.uri = uri 63 | self.mimeType = mimeType 64 | self.text = text 65 | self.blob = blob 66 | } 67 | 68 | /// Creates a new FileResourceContent from a file 69 | /// - Parameters: 70 | /// - fileURL: The URL of the file 71 | /// - mimeType: The MIME type of the resource (optional, will be determined from file extension if nil) 72 | /// - Throws: An error if the file cannot be read 73 | public static func from(fileURL: URL, mimeType: String? = nil) throws -> FileResourceContent { 74 | // Determine MIME type if not provided 75 | let determinedMimeType = mimeType ?? String.mimeType(for: fileURL.pathExtension) 76 | 77 | // Check if it's a text file 78 | let isTextFile = determinedMimeType.hasPrefix("text/") 79 | 80 | if isTextFile { 81 | // Read as text 82 | let text = try String(contentsOf: fileURL, encoding: .utf8) 83 | return FileResourceContent(uri: fileURL, mimeType: determinedMimeType, text: text) 84 | } else { 85 | // Read as binary 86 | let data = try Data(contentsOf: fileURL) 87 | return FileResourceContent(uri: fileURL, mimeType: determinedMimeType, blob: data) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Demos/SwiftMCPDemo/Logging/LoggingSystem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Logging 3 | 4 | #if canImport(OSLog) 5 | import OSLog 6 | 7 | extension LoggingSystem { 8 | /// Bootstrap the logging system with OSLog on Apple platforms 9 | /// - Parameters: 10 | /// - subsystem: The subsystem identifier for OSLog 11 | /// - logLevel: The default log level to use (default: .info) 12 | static func bootstrapWithOSLog(subsystem: String = "com.cocoanetics.SwiftMCP", 13 | logLevel: Logging.Logger.Level = ProcessInfo.processInfo.environment["ENABLE_DEBUG_OUTPUT"] == "1" ? .trace : .info) { 14 | bootstrap { label in 15 | // Create an OSLog-based logger 16 | let category = label.split(separator: ".").last?.description ?? "default" 17 | let osLogger = OSLog(subsystem: subsystem, category: category) 18 | 19 | // Set log level based on parameter 20 | var handler = OSLogHandler(label: label, log: osLogger) 21 | handler.logLevel = logLevel 22 | 23 | return handler 24 | } 25 | } 26 | } 27 | #endif 28 | -------------------------------------------------------------------------------- /Demos/SwiftMCPDemo/Logging/OSLogHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSLogHandler.swift 3 | // SwiftMail 4 | // 5 | // Created by Oliver Drobnik on 04.03.25. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | #if canImport(OSLog) 12 | 13 | @preconcurrency import OSLog 14 | 15 | // Custom LogHandler that bridges Swift Logging to OSLog 16 | struct OSLogHandler: LogHandler { 17 | let label: String 18 | let log: OSLog 19 | 20 | // Required property for LogHandler protocol 21 | var logLevel: Logging.Logger.Level = .debug // Set to debug to capture all logs 22 | 23 | // Required property for LogHandler protocol 24 | var metadata = Logging.Logger.Metadata() 25 | 26 | // Required subscript for LogHandler protocol 27 | subscript(metadataKey metadataKey: String) -> Logging.Logger.Metadata.Value? { 28 | get { 29 | return metadata[metadataKey] 30 | } 31 | set { 32 | metadata[metadataKey] = newValue 33 | } 34 | } 35 | 36 | // Initialize with a label and OSLog instance 37 | init(label: String, log: OSLog) { 38 | self.label = label 39 | self.log = log 40 | } 41 | 42 | // Required method for LogHandler protocol 43 | func log(level: Logging.Logger.Level, message: Logging.Logger.Message, metadata: Logging.Logger.Metadata?, source: String, file: String, function: String, line: UInt) { 44 | // Map Swift Logging levels to OSLog types 45 | let type: OSLogType 46 | switch level { 47 | case .trace, .debug: 48 | type = .debug 49 | case .info, .notice: 50 | type = .info 51 | case .warning: 52 | type = .default 53 | case .error: 54 | type = .error 55 | case .critical: 56 | type = .fault 57 | } 58 | 59 | // Log the message using OSLog 60 | os_log("%{public}@", log: log, type: type, message.description) 61 | } 62 | } 63 | 64 | #endif 65 | -------------------------------------------------------------------------------- /Demos/SwiftMCPDemo/MCPCommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ArgumentParser 3 | import SwiftMCP 4 | import Logging 5 | import NIOCore 6 | import Dispatch 7 | 8 | #if canImport(OSLog) 9 | import OSLog 10 | #endif 11 | 12 | /** 13 | Command-line interface for the SwiftMCP demo. 14 | 15 | This is the main entry point for the SwiftMCP demo application. It provides two modes of operation: 16 | 17 | - `stdio`: The default mode that processes JSON-RPC requests from standard input and writes responses to standard output. 18 | Perfect for integration with other tools via pipes. 19 | 20 | - `httpsse`: Starts an HTTP server with Server-Sent Events (SSE) support, optionally with authentication and OpenAPI endpoints. 21 | Ideal for long-running services and AI plugin integration. 22 | 23 | Example usage: 24 | ```bash 25 | # Using stdio mode (default) 26 | echo '{"jsonrpc": "2.0", "method": "add", "params": [1, 2]}' | SwiftMCPDemo 27 | 28 | # Using HTTP+SSE mode 29 | SwiftMCPDemo httpsse --port 8080 --token secret --openapi 30 | ``` 31 | */ 32 | @main 33 | struct MCPCommand: AsyncParsableCommand { 34 | static let configuration = CommandConfiguration( 35 | commandName: "SwiftMCPDemo", 36 | abstract: "A utility for testing SwiftMCP functions", 37 | discussion: """ 38 | Process JSON-RPC requests for SwiftMCP functions. 39 | 40 | The command can operate in two modes: 41 | 42 | 1. stdio: 43 | - Reads JSON-RPC requests from stdin 44 | - Writes responses to stdout 45 | - Perfect for integration with other tools via pipes 46 | - Example: echo '{"jsonrpc": "2.0", "method": "add", "params": [1, 2]}' | SwiftMCPDemo stdio 47 | 48 | 2. httpsse: 49 | - Starts an HTTP server with Server-Sent Events (SSE) support 50 | - Supports bearer token authentication and OpenAPI endpoints 51 | - Example: SwiftMCPDemo httpsse --port 8080 52 | """, 53 | subcommands: [StdioCommand.self, HTTPSSECommand.self], 54 | defaultSubcommand: StdioCommand.self 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /Demos/SwiftMCPDemo/SignalHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Dispatch 3 | import SwiftMCP 4 | 5 | /// Handles SIGINT signals for graceful shutdown of the HTTP SSE transport 6 | public final class SignalHandler { 7 | /// Actor to manage signal handling state in a thread-safe way 8 | private actor State { 9 | private var sigintSource: DispatchSourceSignal? 10 | private var isShuttingDown = false 11 | private weak var transport: HTTPSSETransport? 12 | 13 | init(transport: HTTPSSETransport) { 14 | self.transport = transport 15 | } 16 | 17 | func setupHandler(on queue: DispatchQueue) { 18 | // Create a dispatch source on the provided queue 19 | sigintSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: queue) 20 | 21 | // Tell the system to ignore the default SIGINT handler 22 | signal(SIGINT, SIG_IGN) 23 | 24 | // Specify what to do when the signal is received 25 | sigintSource?.setEventHandler { [weak self] in 26 | Task { [weak self] in 27 | await self?.handleSignal() 28 | } 29 | } 30 | 31 | // Start listening for the signal 32 | sigintSource?.resume() 33 | } 34 | 35 | private func handleSignal() async { 36 | // Prevent multiple shutdown attempts 37 | guard !isShuttingDown else { return } 38 | isShuttingDown = true 39 | 40 | print("\nShutting down...") 41 | 42 | guard let transport = transport else { 43 | print("Transport no longer available") 44 | Foundation.exit(1) 45 | } 46 | 47 | do { 48 | try await transport.stop() 49 | Foundation.exit(0) 50 | } catch { 51 | print("Error during shutdown: \(error)") 52 | Foundation.exit(1) 53 | } 54 | } 55 | } 56 | 57 | // Instance state 58 | private let state: State 59 | 60 | /// Creates a new signal handler for the given transport 61 | public init(transport: HTTPSSETransport) { 62 | self.state = State(transport: transport) 63 | } 64 | 65 | /// Sets up the SIGINT handler 66 | public func setup() async { 67 | // Create a dedicated dispatch queue for signal handling 68 | let signalQueue = DispatchQueue(label: "com.cocoanetics.signalQueue") 69 | await state.setupHandler(on: signalQueue) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Demos/SwiftMCPDemo/String+MIME.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+MIME.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 10.03.25. 6 | // 7 | 8 | 9 | // String+MIME.swift 10 | // MIME-related extensions for String 11 | 12 | import Foundation 13 | 14 | #if os(macOS) 15 | import UniformTypeIdentifiers 16 | #endif 17 | 18 | extension String { 19 | /// Get a file extension for a given MIME type 20 | /// - Parameter mimeType: The full MIME type (e.g., "text/plain", "image/jpeg") 21 | /// - Returns: An appropriate file extension (without the dot) 22 | public static func fileExtension(for mimeType: String) -> String? { 23 | #if os(macOS) 24 | // Try to get the UTType from the MIME type 25 | if let utType = UTType(mimeType: mimeType) { 26 | // Get the preferred file extension 27 | return utType.preferredFilenameExtension 28 | } 29 | return nil 30 | #else 31 | // Map common MIME types to extensions 32 | let mimeToExtension: [String: String] = [ 33 | "image/jpeg": "jpg", 34 | "image/png": "png", 35 | "image/gif": "gif", 36 | "image/svg+xml": "svg", 37 | "application/pdf": "pdf", 38 | "text/plain": "txt", 39 | "text/html": "html", 40 | "application/msword": "doc", 41 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx", 42 | "application/vnd.ms-excel": "xls", 43 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx", 44 | "application/zip": "zip" 45 | ] 46 | 47 | return mimeToExtension[mimeType] 48 | #endif 49 | } 50 | 51 | /// Get MIME type for a file extension 52 | /// - Parameter fileExtension: The file extension (without dot) 53 | /// - Returns: The corresponding MIME type, or application/octet-stream if unknown 54 | public static func mimeType(for fileExtension: String) -> String { 55 | #if os(macOS) 56 | // Try to get UTType from file extension 57 | if let utType = UTType(filenameExtension: fileExtension), 58 | let mimeType = utType.preferredMIMEType { 59 | return mimeType 60 | } 61 | return "application/octet-stream" 62 | #else 63 | // Map common extensions to MIME types 64 | let extensionToMime: [String: String] = [ 65 | "jpg": "image/jpeg", 66 | "jpeg": "image/jpeg", 67 | "png": "image/png", 68 | "gif": "image/gif", 69 | "svg": "image/svg+xml", 70 | "pdf": "application/pdf", 71 | "txt": "text/plain", 72 | "html": "text/html", 73 | "htm": "text/html", 74 | "doc": "application/msword", 75 | "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 76 | "xls": "application/vnd.ms-excel", 77 | "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 78 | "zip": "application/zip" 79 | ] 80 | 81 | return extensionToMime[fileExtension.lowercased()] ?? "application/octet-stream" 82 | #endif 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025, Oliver Drobnik All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import CompilerPluginSupport 5 | import PackageDescription 6 | 7 | let package = Package( 8 | name: "SwiftMCP", 9 | platforms: [ 10 | .macOS("11.0"), 11 | .iOS("14.0"), 12 | .tvOS("14.0"), 13 | .watchOS("7.0"), 14 | .macCatalyst("14.0") 15 | ], 16 | products: [ 17 | .library( 18 | name: "SwiftMCP", 19 | targets: ["SwiftMCP"] 20 | ), 21 | .executable( 22 | name: "SwiftMCPDemo", 23 | targets: ["SwiftMCPDemo"] 24 | ) 25 | ], 26 | dependencies: [ 27 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), 28 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"), 29 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"), 30 | .package(url: "https://github.com/Flight-School/AnyCodable", from: "0.6.0"), 31 | .package(url: "https://github.com/swiftlang/swift-docc-plugin.git", from: "1.0.0"), 32 | .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "602.0.0-latest") 33 | ], 34 | targets: [ 35 | .macro( 36 | name: "SwiftMCPMacros", 37 | dependencies: [ 38 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 39 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 40 | ] 41 | ), 42 | .target( 43 | name: "SwiftMCP", 44 | dependencies: [ 45 | "AnyCodable", 46 | "SwiftMCPMacros", 47 | .product(name: "NIOCore", package: "swift-nio"), 48 | .product(name: "NIOHTTP1", package: "swift-nio"), 49 | .product(name: "NIOPosix", package: "swift-nio"), 50 | .product(name: "Logging", package: "swift-log"), 51 | .product(name: "NIOFoundationCompat", package: "swift-nio") 52 | ] 53 | ), 54 | .executableTarget( 55 | name: "SwiftMCPDemo", 56 | dependencies: [ 57 | "SwiftMCP", 58 | .product(name: "ArgumentParser", package: "swift-argument-parser") 59 | ], 60 | path: "Demos/SwiftMCPDemo" 61 | ), 62 | .testTarget( 63 | name: "SwiftMCPTests", 64 | dependencies: ["SwiftMCP", "SwiftMCPMacros"] 65 | ) 66 | ] 67 | ) 68 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Errors/MCPResourceError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Errors that can occur when working with MCP resources 4 | public enum MCPResourceError: LocalizedError { 5 | /// The requested resource was not found 6 | case notFound(uri: String) 7 | 8 | /// The URI template does not match the provided URI 9 | case templateMismatch(template: String, uri: String) 10 | 11 | /// A required parameter is missing 12 | case missingParameter(name: String) 13 | 14 | /// Parameter type conversion failed 15 | case typeMismatch(parameter: String, expectedType: String, actualValue: String) 16 | 17 | /// Internal execution error 18 | case executionError(underlying: Error) 19 | 20 | /// Invalid URI template 21 | case invalidTemplate(template: String) 22 | 23 | public var errorDescription: String? { 24 | switch self { 25 | case .notFound(let uri): 26 | return "Resource not found: \(uri)" 27 | case .templateMismatch(let template, let uri): 28 | return "URI '\(uri)' does not match template '\(template)'" 29 | case .missingParameter(let name): 30 | return "Missing required parameter: \(name)" 31 | case .typeMismatch(let parameter, let expectedType, let actualValue): 32 | return "Parameter '\(parameter)' type mismatch: expected \(expectedType), got '\(actualValue)'" 33 | case .executionError(let error): 34 | return "Resource execution error: \(error.localizedDescription)" 35 | case .invalidTemplate(let template): 36 | return "Invalid URI template: \(template)" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/Array+CaseIterableElements.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+CaseIterableElements.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 07.04.25. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ArrayWithCaseIterableElements { 11 | static func schema(description: String?) -> JSONSchema 12 | } 13 | 14 | extension Array: ArrayWithCaseIterableElements where Element: CaseIterable { 15 | 16 | public static func schema(description: String? = nil) -> JSONSchema { 17 | 18 | let elementSchema = JSONSchema.enum(values: Element.caseLabels) 19 | return .array(items: elementSchema, description: description) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/Array+CaseLabels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+CaseLabels.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 26.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | An extension on Array that provides functionality for extracting case labels from CaseIterable types. 12 | 13 | This extension allows creating an array of strings from the case labels of any type that conforms to CaseIterable. 14 | The case labels are extracted using the type's string representation, with special handling for cases with associated values. 15 | 16 | If the enum conforms to CustomStringConvertible, the case labels will be determined by the custom description implementation. 17 | This allows for customization of how enum cases are represented in MCP tools. 18 | */ 19 | extension Array where Element == String { 20 | /** 21 | Initialize an array of case labels if the given parameter (a type) conforms to CaseIterable. 22 | 23 | - Parameters: 24 | - type: The type to extract case labels from. Must conform to CaseIterable. 25 | 26 | - Returns: An array of strings containing the case labels, or nil if the type doesn't conform to CaseIterable. 27 | 28 | - Note: For cases with associated values, this initializer will extract the case name without the associated values. 29 | For example, for a case like `case example(value: Int)`, it will return `"example"`. 30 | 31 | - Note: If the enum conforms to CustomStringConvertible, the case labels will be determined by the custom description implementation. 32 | This allows for customization of how enum cases are represented in MCP tools. 33 | */ 34 | public init?(caseLabelsFrom type: T.Type) { 35 | // Check if T conforms to CaseIterable at runtime. 36 | guard let caseIterableType = type as? any CaseIterable.Type else { 37 | return nil 38 | } 39 | 40 | let cases = caseIterableType.allCases 41 | self = cases.map { caseValue in 42 | let description = String(describing: caseValue) 43 | 44 | // trim off associated value if any 45 | if let parenIndex = description.firstIndex(of: "(") { 46 | return String(description[.. [Prompt] { 6 | self.map { meta in 7 | Prompt(name: meta.name, description: meta.description, arguments: meta.parameters) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/Array+MCPTool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+MCPTool.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 08.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array where Element == MCPToolMetadata { 11 | public func convertedToTools() -> [MCPTool] { 12 | return self.map { meta in 13 | // Create properties for the JSON schema 14 | let properties = Dictionary(uniqueKeysWithValues: meta.parameters.map { param in 15 | return (param.name, param.schema) 16 | }) 17 | 18 | // Determine which parameters are required using the isRequired property 19 | let required = meta.parameters.filter { $0.isRequired }.map { $0.name } 20 | 21 | // Create the input schema 22 | let inputSchema = JSONSchema.object(JSONSchema.Object( 23 | properties: properties, 24 | required: required, 25 | description: meta.description 26 | )) 27 | 28 | // Create and return the tool 29 | return MCPTool( 30 | name: meta.name, 31 | description: meta.description, 32 | inputSchema: inputSchema 33 | ) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/Array+SchemaRepresentableElements.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ArrayWithSchemaRepresentableElements { 4 | static func schema(description: String?) -> JSONSchema 5 | } 6 | 7 | extension Array: ArrayWithSchemaRepresentableElements where Element: SchemaRepresentable { 8 | 9 | public static func schema(description: String? = nil) -> JSONSchema { 10 | 11 | return .array(items: Element.schemaMetadata.schema, description: description) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/BinaryFloatingPoint+Conversion.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension BinaryFloatingPoint { 4 | /// Attempts to convert the given value to `Self`. 5 | /// Returns `nil` if the conversion is not possible. 6 | static func convert(from value: Any) -> Self? { 7 | if let this = value as? Self { 8 | return this 9 | } 10 | if let boolValue = value as? Bool { 11 | return boolValue ? 1 : 0 12 | } 13 | if let integerValue = value as? any BinaryInteger { 14 | return Self(integerValue) 15 | } 16 | if let floatingValue = value as? any BinaryFloatingPoint { 17 | return Self(floatingValue) 18 | } 19 | return nil 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/BinaryInteger+Conversion.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension BinaryInteger { 4 | /// Attempts to convert the given value to `Self`. 5 | /// Returns `nil` if the conversion is not possible. 6 | static func convert(from value: Any) -> Self? { 7 | if let this = value as? Self { 8 | return this 9 | } 10 | if let boolValue = value as? Bool { 11 | return boolValue ? 1 : 0 12 | } 13 | if let integerValue = value as? any BinaryInteger { 14 | return Self(exactly: integerValue) 15 | } 16 | if let floatingValue = value as? any BinaryFloatingPoint { 17 | return Self(exactly: floatingValue) 18 | } 19 | return nil 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/BinaryNumeric+Convert.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension BinaryInteger { 4 | static func convert(from value: T) -> Self? { 5 | if let exact = value as? Self { 6 | return exact 7 | } 8 | if let boolValue = value as? Bool { 9 | return Self(boolValue ? 1 : 0) 10 | } 11 | if let intValue = value as? any BinaryInteger { 12 | return Self(intValue) // This should generally work if Self can represent intValue 13 | } 14 | if let floatValue = value as? any BinaryFloatingPoint { 15 | let doubleValue = Double(floatValue) // Convert to Double for consistent checks 16 | // Ensure no precision is lost for integers 17 | if doubleValue.truncatingRemainder(dividingBy: 1.0) == 0 { 18 | return Self(exactly: doubleValue) // Attempt to initialize from Double 19 | } 20 | } 21 | // Attempt to convert from String 22 | if let stringValue = value as? String { 23 | // Try initializing from common integer string representations 24 | if let intVal = Int(stringValue) { return Self(exactly: intVal) } 25 | if let int64Val = Int64(stringValue) { return Self(exactly: int64Val) } 26 | // Add UInt, UInt64 etc. if necessary and if Self supports them 27 | // Fallback to a general radix-based init if available and needed, though less common here. 28 | } 29 | return nil 30 | } 31 | } 32 | 33 | public extension BinaryFloatingPoint { 34 | static func convert(from value: T) -> Self? { 35 | if let exact = value as? Self { 36 | return exact 37 | } 38 | if let boolValue = value as? Bool { 39 | return Self(boolValue ? 1.0 : 0.0) 40 | } 41 | if let intValue = value as? any BinaryInteger { 42 | return Self(intValue) 43 | } 44 | if let floatValue = value as? any BinaryFloatingPoint { 45 | return Self(floatValue) // Direct conversion if T is also some BinaryFloatingPoint 46 | } 47 | // Attempt to convert from String 48 | if let stringValue = value as? String { 49 | // Convert string to Double first, then to Self. 50 | // This is a common strategy as Double(String) is robust. 51 | if let doubleVal = Double(stringValue) { 52 | return Self(doubleVal) // Assumes Self can be initialized from Double 53 | } 54 | } 55 | return nil 56 | } 57 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/JSON+DateEncoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSON+DateEncoding.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 07.04.25. 6 | // 7 | 8 | import Foundation 9 | 10 | extension ISO8601DateFormatter: @retroactive @unchecked Sendable {} 11 | 12 | extension JSONEncoder.DateEncodingStrategy { 13 | private static let iso8601Formatter: ISO8601DateFormatter = { 14 | let formatter = ISO8601DateFormatter() 15 | formatter.timeZone = TimeZone.current 16 | formatter.formatOptions = [.withInternetDateTime, .withTimeZone] 17 | return formatter 18 | }() 19 | 20 | static let iso8601WithTimeZone = JSONEncoder.DateEncodingStrategy.custom { date, encoder in 21 | let string = iso8601Formatter.string(from: date) 22 | var container = encoder.singleValueContainer() 23 | try container.encode(string) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/JSONRPCMessage+Batch.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOCore 3 | 4 | extension JSONRPCMessage { 5 | /// Decode a single or batched JSON-RPC payload from `Data`. 6 | /// - Parameter data: Raw JSON data. 7 | /// - Returns: An array of `JSONRPCMessage` items. 8 | static func decodeMessages(from data: Data) throws -> [JSONRPCMessage] { 9 | let decoder = JSONDecoder() 10 | decoder.dateDecodingStrategy = .iso8601 11 | if let batch = try? decoder.decode([JSONRPCMessage].self, from: data) { 12 | return batch 13 | } else { 14 | let single = try decoder.decode(JSONRPCMessage.self, from: data) 15 | return [single] 16 | } 17 | } 18 | 19 | /// Decode a single or batched JSON-RPC payload from a `ByteBuffer`. 20 | /// - Parameter buffer: Incoming buffer containing JSON data. 21 | static func decodeMessages(from buffer: ByteBuffer) throws -> [JSONRPCMessage] { 22 | var copy = buffer 23 | if let data = copy.readData(length: copy.readableBytes) { 24 | return try decodeMessages(from: data) 25 | } 26 | let decoder = JSONDecoder() 27 | decoder.dateDecodingStrategy = .iso8601 28 | if let batch = try? decoder.decode([JSONRPCMessage].self, from: buffer) { 29 | return batch 30 | } else { 31 | let single = try decoder.decode(JSONRPCMessage.self, from: buffer) 32 | return [single] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/MCPParameterInfo+Completion.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension MCPParameterInfo { 4 | /// Provides default completion suggestions for this parameter. 5 | /// - Parameter prefix: The prefix already typed by the client. 6 | /// - Returns: Completion result based on the parameter's type. 7 | func defaultCompletion(prefix: String) -> CompleteResult.Completion { 8 | guard let caseType = type as? any CaseIterable.Type else { 9 | return CompleteResult.Completion(values: []) 10 | } 11 | 12 | let values = caseType.caseLabels.sortedByBestCompletion(prefix: prefix) 13 | return CompleteResult.Completion(values: values, total: values.count, hasMore: false) 14 | } 15 | 16 | /// Returns enum completions for this parameter if it is CaseIterable. 17 | /// - Parameter prefix: The prefix already typed by the client. 18 | /// - Returns: Completion result or nil if the parameter isn't an enum. 19 | func defaultEnumCompletion(prefix: String) -> CompleteResult.Completion? { 20 | guard type is any CaseIterable.Type else { return nil } 21 | return defaultCompletion(prefix: prefix) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/MCPParameterInfo+JSONSchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPParameterInfo+JSONSchema.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 18.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ArraySchemaBridge { 11 | static var elementType: Any.Type { get } 12 | } 13 | 14 | extension Array: ArraySchemaBridge { 15 | static var elementType: Any.Type { Element.self } 16 | } 17 | 18 | extension MCPParameterInfo { 19 | 20 | public var schema: JSONSchema { 21 | // If this is a SchemaRepresentable type, use its schema 22 | if let schemaType = type as? any SchemaRepresentable.Type { 23 | return schemaType.schemaMetadata.schema 24 | } 25 | 26 | // If this is a CaseIterable type, return a string schema with enum values 27 | if let caseIterableType = type as? any CaseIterable.Type { 28 | return JSONSchema.enum(values: caseIterableType.caseLabels, description: description) 29 | } 30 | 31 | // Handle array types 32 | if let arrayType = type as? ArrayWithSchemaRepresentableElements.Type { 33 | 34 | return arrayType.schema(description: description) 35 | } 36 | 37 | if let arrayType = type as? ArrayWithCaseIterableElements.Type { 38 | 39 | return arrayType.schema(description: description) 40 | } 41 | 42 | if let arrayBridge = type as? ArraySchemaBridge.Type { 43 | // Get the element type from the array 44 | let type = arrayBridge.elementType 45 | 46 | let schema: JSONSchema 47 | 48 | if type == Int.self || type == Double.self { 49 | schema = JSONSchema.number() 50 | } else if type == Bool.self { 51 | schema = JSONSchema.boolean() 52 | } else if let schemaType = type as? any SchemaRepresentable.Type { 53 | schema = schemaType.schemaMetadata.schema 54 | } else { 55 | schema = JSONSchema.string() 56 | } 57 | 58 | return JSONSchema.array(items: schema, description: description) 59 | } 60 | 61 | // Handle basic types 62 | switch type { 63 | case is Int.Type, is Double.Type: 64 | return JSONSchema.number(description: description) 65 | case is Bool.Type: 66 | return JSONSchema.boolean(description: description) 67 | default: 68 | return JSONSchema.string(description: description) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/MCPServer+Batch.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension MCPServer { 4 | /// Processes a batch of JSON-RPC messages sequentially. 5 | /// - Parameters: 6 | /// - messages: The messages to handle. 7 | /// - ignoringEmptyResponses: If true, `.response` messages with an empty result are ignored. 8 | /// - Returns: An array of response messages. 9 | func processBatch(_ messages: [JSONRPCMessage], ignoringEmptyResponses: Bool = false) async -> [JSONRPCMessage] { 10 | var responses: [JSONRPCMessage] = [] 11 | for message in messages { 12 | if ignoringEmptyResponses, 13 | case .response(let responseData) = message, 14 | let result = responseData.result, 15 | result.isEmpty { 16 | continue 17 | } 18 | 19 | if let response = await handleMessage(message) { 20 | responses.append(response) 21 | } 22 | } 23 | return responses 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/MCPToolMetadata+Arguments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPToolMetadata+Arguments.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 28.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Extension to provide utility methods for working with MCP tools. 12 | */ 13 | extension MCPToolMetadata { 14 | /** 15 | Enriches a dictionary of arguments with default values and throws an error if a required parameter is missing. 16 | 17 | - Parameters: 18 | - arguments: The dictionary of arguments to enrich 19 | - functionName: The name of the function being called (for error messages) 20 | 21 | - Returns: The enriched dictionary of arguments 22 | - Throws: An error if a required parameter is missing 23 | */ 24 | public func enrichArguments(_ arguments: [String: Sendable], functionName: String? = nil) throws -> [String: Sendable] { 25 | var enrichedArguments = arguments 26 | 27 | // Add default values for missing parameters 28 | for param in parameters { 29 | if enrichedArguments[param.name] == nil { 30 | if let defaultValue = param.defaultValue { 31 | enrichedArguments[param.name] = defaultValue 32 | } else if param.isRequired { 33 | throw MCPToolError.missingRequiredParameter(parameterName: param.name) 34 | } 35 | } 36 | } 37 | 38 | return enrichedArguments 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/String+CompletionSorting.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Array where Element == String { 4 | /// Returns the array sorted so that strings with the longest prefix match 5 | /// come first. The comparison is case-insensitive. 6 | /// - Parameter prefix: The prefix typed by the client. 7 | func sortedByBestCompletion(prefix: String) -> [String] { 8 | let lower = prefix.lowercased() 9 | return self.enumerated().sorted { lhs, rhs in 10 | let lMatch = lhs.element.lowercased().commonPrefix(with: lower).count 11 | let rMatch = rhs.element.lowercased().commonPrefix(with: lower).count 12 | if lMatch == rMatch { 13 | return lhs.offset < rhs.offset 14 | } 15 | return lMatch > rMatch 16 | }.map { $0.element } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/String+ContentType.swift: -------------------------------------------------------------------------------- 1 | extension String { 2 | /// Checks if the content type matches the Accept header string 3 | /// - Parameter acceptHeader: The Accept header string (e.g. "text/html, application/xhtml+xml, */*") 4 | /// - Returns: true if the content type matches any of the accepted types 5 | func matchesAcceptHeader(_ acceptHeader: String) -> Bool { 6 | // Split accept header into individual types 7 | let acceptedTypes = acceptHeader.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) } 8 | 9 | // Get the main type and subtype of the content type (self) 10 | let contentParts = self.split(separator: "/") 11 | guard contentParts.count == 2 else { return false } 12 | let contentMainType = String(contentParts[0]) 13 | let contentSubType = String(contentParts[1]) 14 | 15 | for acceptedType in acceptedTypes { 16 | // Handle quality values (;q=0.9) by removing them 17 | let type = acceptedType.split(separator: ";")[0].trimmingCharacters(in: .whitespaces) 18 | 19 | // Handle */* case 20 | if type == "*/*" { 21 | return true 22 | } 23 | 24 | let parts = type.split(separator: "/") 25 | guard parts.count == 2 else { continue } 26 | 27 | let mainType = String(parts[0]) 28 | let subType = String(parts[1]) 29 | 30 | // Check for exact match 31 | if mainType == contentMainType && (subType == "*" || subType == contentSubType) { 32 | return true 33 | } 34 | 35 | // Check for type/* match 36 | if mainType == "*" && subType == "*" { 37 | return true 38 | } 39 | } 40 | 41 | return false 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/String+Hostname.swift: -------------------------------------------------------------------------------- 1 | // String+Hostname.swift 2 | // Hostname and IP-related extensions for String 3 | 4 | #if canImport(Darwin) 5 | import Darwin 6 | #else 7 | import Glibc 8 | #endif 9 | 10 | import Foundation 11 | 12 | 13 | extension String { 14 | /** 15 | Get the local hostname for EHLO/HELO commands 16 | - Returns: The local hostname 17 | */ 18 | public static var localHostname: String { 19 | #if os(macOS) && !targetEnvironment(macCatalyst) 20 | // Host is only available on macOS 21 | if let hostname = Host.current().name { 22 | return hostname 23 | } 24 | #else 25 | // Use system call on Linux and other platforms 26 | var hostname = [CChar](repeating: 0, count: 256) // Linux typically uses 256 as max hostname length. 27 | if gethostname(&hostname, hostname.count) == 0 { 28 | // Create a string from the C string 29 | if let name = String(cString: hostname, encoding: .utf8), !name.isEmpty { 30 | return name 31 | } 32 | } 33 | #endif 34 | 35 | return "localhost" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/String+OpenAPI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | /// Formats a string to be used as a model name: 5 | /// - Converts to lowercase 6 | /// - Splits on non-alphanumeric characters 7 | /// - Joins with underscores 8 | var asModelName: String { 9 | self.lowercased() 10 | .trimmingCharacters(in: .whitespacesAndNewlines) 11 | .components(separatedBy: CharacterSet.alphanumerics.inverted) 12 | .filter { !$0.isEmpty } 13 | .joined(separator: "_") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/String+Quotes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Quotes.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 07.04.25. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension String { 11 | var removingQuotes: String { 12 | 13 | guard first == "\"" && last == "\"" else { 14 | return self 15 | } 16 | 17 | return String(dropFirst().dropLast()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Extensions/Type+JSONSchema.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Protocol for types that can be converted to JSON Schema types 4 | public protocol JSONSchemaTypeConvertible { 5 | /// The JSON Schema representation for this type 6 | static func jsonSchema(description: String?) -> JSONSchema 7 | } 8 | 9 | // Add automatic conformance for CaseIterable types 10 | extension CaseIterable { 11 | public static func jsonSchema(description: String?) -> JSONSchema { 12 | .enum(values: caseLabels, description: description) 13 | } 14 | } 15 | 16 | extension Int: JSONSchemaTypeConvertible { 17 | public static func jsonSchema(description: String?) -> JSONSchema { 18 | .number(description: description) 19 | } 20 | } 21 | 22 | extension UInt: JSONSchemaTypeConvertible { 23 | public static func jsonSchema(description: String?) -> JSONSchema { 24 | .number(description: description) 25 | } 26 | } 27 | 28 | extension Int8: JSONSchemaTypeConvertible { 29 | public static func jsonSchema(description: String?) -> JSONSchema { 30 | .number(description: description) 31 | } 32 | } 33 | 34 | extension Int16: JSONSchemaTypeConvertible { 35 | public static func jsonSchema(description: String?) -> JSONSchema { 36 | .number(description: description) 37 | } 38 | } 39 | 40 | extension Int32: JSONSchemaTypeConvertible { 41 | public static func jsonSchema(description: String?) -> JSONSchema { 42 | .number(description: description) 43 | } 44 | } 45 | 46 | extension Int64: JSONSchemaTypeConvertible { 47 | public static func jsonSchema(description: String?) -> JSONSchema { 48 | .number(description: description) 49 | } 50 | } 51 | 52 | extension UInt8: JSONSchemaTypeConvertible { 53 | public static func jsonSchema(description: String?) -> JSONSchema { 54 | .number(description: description) 55 | } 56 | } 57 | 58 | extension UInt16: JSONSchemaTypeConvertible { 59 | public static func jsonSchema(description: String?) -> JSONSchema { 60 | .number(description: description) 61 | } 62 | } 63 | 64 | extension UInt32: JSONSchemaTypeConvertible { 65 | public static func jsonSchema(description: String?) -> JSONSchema { 66 | .number(description: description) 67 | } 68 | } 69 | 70 | extension UInt64: JSONSchemaTypeConvertible { 71 | public static func jsonSchema(description: String?) -> JSONSchema { 72 | .number(description: description) 73 | } 74 | } 75 | 76 | extension Float: JSONSchemaTypeConvertible { 77 | public static func jsonSchema(description: String?) -> JSONSchema { 78 | .number(description: description) 79 | } 80 | } 81 | 82 | extension Double: JSONSchemaTypeConvertible { 83 | public static func jsonSchema(description: String?) -> JSONSchema { 84 | .number(description: description) 85 | } 86 | } 87 | 88 | extension Bool: JSONSchemaTypeConvertible { 89 | public static func jsonSchema(description: String?) -> JSONSchema { 90 | .boolean(description: description) 91 | } 92 | } 93 | 94 | extension String: JSONSchemaTypeConvertible { 95 | public static func jsonSchema(description: String?) -> JSONSchema { 96 | .string(description: description) 97 | } 98 | } 99 | 100 | extension Character: JSONSchemaTypeConvertible { 101 | public static func jsonSchema(description: String?) -> JSONSchema { 102 | .string(description: description) 103 | } 104 | } 105 | 106 | extension Data: JSONSchemaTypeConvertible { 107 | public static func jsonSchema(description: String?) -> JSONSchema { 108 | .string(description: description, format: "byte") 109 | } 110 | } 111 | 112 | extension Array: JSONSchemaTypeConvertible { 113 | public static func jsonSchema(description: String?) -> JSONSchema { 114 | let elementSchema: JSONSchema 115 | 116 | if let elementType = Element.self as? any JSONSchemaTypeConvertible.Type { 117 | elementSchema = elementType.jsonSchema(description: nil) 118 | } else if let schemaType = Element.self as? any SchemaRepresentable.Type { 119 | elementSchema = schemaType.schemaMetadata.schema 120 | } else if let caseIterableType = Element.self as? any CaseIterable.Type { 121 | elementSchema = .enum(values: caseIterableType.caseLabels) 122 | } else { 123 | elementSchema = .string() 124 | } 125 | 126 | return .array(items: elementSchema, description: description) 127 | } 128 | } 129 | 130 | extension Dictionary: JSONSchemaTypeConvertible { 131 | public static func jsonSchema(description: String?) -> JSONSchema { 132 | .object(JSONSchema.Object(properties: [:], required: [], description: description)) 133 | } 134 | } 135 | 136 | extension Optional: JSONSchemaTypeConvertible { 137 | public static func jsonSchema(description: String?) -> JSONSchema { 138 | if let wrappedType = Wrapped.self as? any JSONSchemaTypeConvertible.Type { 139 | return wrappedType.jsonSchema(description: description) 140 | } else if let schemaType = Wrapped.self as? any SchemaRepresentable.Type { 141 | return schemaType.schemaMetadata.schema 142 | } else if let caseIterableType = Wrapped.self as? any CaseIterable.Type { 143 | return .enum(values: caseIterableType.caseLabels, description: description) 144 | } 145 | return .string(description: description) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/MCPMacros.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPMacros.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 08.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Macros for the Model Context Protocol (MCP). 12 | 13 | This file contains macro declarations that are used to automatically 14 | generate metadata for functions and classes in the MCP. 15 | */ 16 | 17 | /// A macro that automatically extracts parameter information from a function declaration. 18 | /// 19 | /// Apply this macro to functions that should be exposed to AI models. 20 | /// It will generate metadata about the function's parameters, return type, and description. 21 | /// 22 | /// Example: 23 | /// ```swift 24 | /// @MCPTool(description: "Adds two numbers") 25 | /// func add(a: Int, b: Int) -> Int { 26 | /// return a + b 27 | /// } 28 | /// ``` 29 | @attached(peer, names: prefixed(__mcpMetadata_), prefixed(__mcpCall_)) 30 | public macro MCPTool(description: String? = nil, isConsequential: Bool = true) = #externalMacro(module: "SwiftMCPMacros", type: "MCPToolMacro") 31 | 32 | /// A macro that adds a `mcpTools` property to a class to aggregate function metadata. 33 | /// 34 | /// Apply this macro to classes that contain `MCPTool` annotated methods. 35 | /// It will generate a property that returns an array of `MCPTool` objects 36 | /// representing all the functions in the class. 37 | /// It also automatically adds the `MCPServer` protocol conformance. 38 | /// 39 | /// Example: 40 | /// ```swift 41 | /// @MCPServer 42 | /// class Calculator { 43 | /// @MCPTool(description: "Adds two numbers") 44 | /// func add(a: Int, b: Int) -> Int { 45 | /// return a + b 46 | /// } 47 | /// } 48 | /// ``` 49 | @attached(member, names: named(callTool), named(mcpToolMetadata), named(__mcpServerName), named(__mcpServerVersion), named(__mcpServerDescription), named(mcpResourceMetadata), named(mcpResources), named(mcpStaticResources), named(mcpResourceTemplates), named(getResource), named(__callResourceFunction), named(callResourceAsFunction), named(mcpPromptMetadata), named(callPrompt)) 50 | @attached(extension, conformances: MCPServer, MCPToolProviding, MCPResourceProviding, MCPPromptProviding) 51 | public macro MCPServer(name: String? = nil, version: String? = nil) = #externalMacro(module: "SwiftMCPMacros", type: "MCPServerMacro") 52 | 53 | /// A macro that generates schema metadata for a struct. 54 | /// 55 | /// Apply this macro to structs to generate metadata about their properties, 56 | /// including property names, types, descriptions, and default values. 57 | /// The macro extracts documentation from comments and generates a hidden 58 | /// metadata property that can be used for validation and serialization. 59 | /// 60 | /// Example: 61 | /// ```swift 62 | /// /// A person's contact information 63 | /// @Schema 64 | /// struct ContactInfo { 65 | /// /// The person's full name 66 | /// let name: String 67 | /// 68 | /// /// The person's email address 69 | /// let email: String 70 | /// 71 | /// /// The person's phone number (optional) 72 | /// let phone: String? 73 | /// } 74 | /// ``` 75 | @attached(member, names: named(schemaMetadata)) 76 | @attached(extension, conformances: SchemaRepresentable) 77 | public macro Schema() = #externalMacro(module: "SwiftMCPMacros", type: "SchemaMacro") 78 | 79 | /// Macro for validating resource functions against a URI template. 80 | /// 81 | /// Apply this macro to functions that should be exposed as MCP resources. 82 | /// It will generate metadata about the function's parameters, return type, and URI template. 83 | /// 84 | /// Example usage: 85 | /// ```swift 86 | /// @MCPResource("users://{user_id}/profile?locale={lang}") 87 | /// func getUserProfile(user_id: Int, lang: String = "en") -> ProfileResource 88 | /// 89 | /// @MCPResource(["users://{user_id}/profile", "users://{user_id}"]) 90 | /// func getUserProfile(user_id: Int, lang: String = "en") -> ProfileResource 91 | /// ``` 92 | @attached(peer, names: prefixed(__mcpResourceMetadata_), prefixed(__mcpResourceCall_)) 93 | public macro MCPResource(_ template: T, name: String? = nil, mimeType: String? = nil) = #externalMacro(module: "SwiftMCPMacros", type: "MCPResourceMacro") 94 | 95 | @attached(peer, names: prefixed(__mcpPromptMetadata_), prefixed(__mcpPromptCall_)) 96 | public macro MCPPrompt(description: String? = nil) = #externalMacro(module: "SwiftMCPMacros", type: "MCPPromptMacro") 97 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Completion/CompleteRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents a completion request referencing a prompt or resource. 4 | public struct CompleteRequest: Codable, Sendable { 5 | /// Identifies the argument being completed. 6 | public struct Argument: Codable, Sendable { 7 | public let name: String 8 | public let value: String? 9 | 10 | public init(name: String, value: String? = nil) { 11 | self.name = name 12 | self.value = value 13 | } 14 | } 15 | 16 | /// Reference to the prompt or resource context for the completion. 17 | public enum Reference: Codable, Sendable { 18 | case prompt(name: String) 19 | case resource(uri: String) 20 | 21 | private enum CodingKeys: String, CodingKey { case type, name, uri } 22 | 23 | public init(from decoder: Decoder) throws { 24 | let container = try decoder.container(keyedBy: CodingKeys.self) 25 | let type = try container.decode(String.self, forKey: .type) 26 | switch type { 27 | case "ref/prompt": 28 | let name = try container.decode(String.self, forKey: .name) 29 | self = .prompt(name: name) 30 | case "ref/resource": 31 | let uri = try container.decode(String.self, forKey: .uri) 32 | self = .resource(uri: uri) 33 | default: 34 | throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown reference type") 35 | } 36 | } 37 | 38 | public func encode(to encoder: Encoder) throws { 39 | var container = encoder.container(keyedBy: CodingKeys.self) 40 | switch self { 41 | case .prompt(let name): 42 | try container.encode("ref/prompt", forKey: .type) 43 | try container.encode(name, forKey: .name) 44 | case .resource(let uri): 45 | try container.encode("ref/resource", forKey: .type) 46 | try container.encode(uri, forKey: .uri) 47 | } 48 | } 49 | } 50 | 51 | public let ref: Reference 52 | public let argument: Argument 53 | 54 | public init(ref: Reference, argument: Argument) { 55 | self.ref = ref 56 | self.argument = argument 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Completion/CompleteResult.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents the completion results returned by the server. 4 | public struct CompleteResult: Codable, Sendable { 5 | /// The completion information containing the suggested values. 6 | public struct Completion: Codable, Sendable { 7 | public let values: [String] 8 | public let total: Int? 9 | public let hasMore: Bool? 10 | 11 | public init(values: [String], total: Int? = nil, hasMore: Bool? = nil) { 12 | self.values = values 13 | self.total = total 14 | self.hasMore = hasMore 15 | } 16 | } 17 | 18 | public let completion: Completion 19 | 20 | public init(values: [String], total: Int? = nil, hasMore: Bool? = nil) { 21 | self.completion = Completion(values: values, total: total, hasMore: hasMore) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Initialization/ServerCapabilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // defines.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 03.04.25. 6 | // 7 | 8 | import Foundation 9 | @preconcurrency import AnyCodable 10 | 11 | /// Represents the capabilities of an MCP server. 12 | /// 13 | /// This struct defines the various capabilities that an MCP server can support, 14 | /// including experimental features, logging, completions, prompts, resources, and tools. 15 | /// These capabilities are communicated to clients during initialization to inform them 16 | /// about what functionalities are available on the server. 17 | public struct ServerCapabilities: Codable, Sendable { 18 | /// Experimental, non-standard capabilities that the server supports. 19 | public var experimental: [String: AnyCodable] = [:] 20 | 21 | /// Present if the server supports sending log messages to the client. 22 | public var logging: AnyCodable? 23 | 24 | /// Present if the server supports argument autocompletion suggestions. 25 | public var completions: AnyCodable? 26 | 27 | /// Present if the server offers any prompt templates. 28 | public var prompts: PromptsCapabilities? 29 | 30 | /// Present if the server offers any resources to read. 31 | public var resources: ResourcesCapabilities? 32 | 33 | /// Present if the server offers any tools to call. 34 | public var tools: ToolsCapabilities? 35 | 36 | /// Capabilities related to prompt templates. 37 | public struct PromptsCapabilities: Codable, Sendable { 38 | /// Whether this server supports notifications for changes to the prompt list. 39 | public var listChanged: Bool? 40 | 41 | public init(listChanged: Bool? = nil) { 42 | self.listChanged = listChanged 43 | } 44 | } 45 | 46 | /// Capabilities related to resources. 47 | public struct ResourcesCapabilities: Codable, Sendable { 48 | /// Whether this server supports subscribing to resource updates. 49 | public var subscribe: Bool? 50 | 51 | /// Whether this server supports notifications for changes to the resource list. 52 | public var listChanged: Bool? 53 | 54 | public init(subscribe: Bool? = nil, listChanged: Bool? = nil) { 55 | self.subscribe = subscribe 56 | self.listChanged = listChanged 57 | } 58 | } 59 | 60 | /// Capabilities related to tools. 61 | public struct ToolsCapabilities: Codable, Sendable { 62 | /// Whether this server supports notifications for changes to the tool list. 63 | public var listChanged: Bool? 64 | 65 | public init(listChanged: Bool? = nil) { 66 | self.listChanged = listChanged 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/InitializeResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InitializeResult.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 18.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The result structure for initialize response 11 | public struct InitializeResult: Codable, Sendable { 12 | /// The protocol version supported by the server 13 | public let protocolVersion: String 14 | 15 | /// The server's capabilities 16 | public let capabilities: ServerCapabilities 17 | 18 | /// Information about the server 19 | public let serverInfo: ServerInfo 20 | 21 | /// Server information structure 22 | public struct ServerInfo: Codable, Sendable { 23 | /// The name of the server 24 | public let name: String 25 | 26 | /// The version of the server 27 | public let version: String 28 | 29 | public init(name: String, version: String) { 30 | self.name = name 31 | self.version = version 32 | } 33 | } 34 | 35 | public init(protocolVersion: String, capabilities: ServerCapabilities, serverInfo: ServerInfo) { 36 | self.protocolVersion = protocolVersion 37 | self.capabilities = capabilities 38 | self.serverInfo = serverInfo 39 | } 40 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/JSONSchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONSchema.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 08.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A simplified representation of JSON Schema for use in the macros 11 | public indirect enum JSONSchema: Sendable { 12 | /** 13 | A structured schema type 14 | */ 15 | public struct Object: Sendable 16 | { 17 | /// The properties of the type 18 | public var properties: [String: JSONSchema] 19 | 20 | /// Which if the properties are mandatory 21 | public var required: [String] = [] 22 | 23 | /// Description of the type 24 | public var description: String? = nil 25 | 26 | /// Whether additional properties are allowed 27 | public var additionalProperties: Bool? = false 28 | 29 | /// public initializer 30 | public init(properties: [String : JSONSchema], required: [String], description: String? = nil, additionalProperties: Bool? = nil) { 31 | self.properties = properties 32 | self.required = required 33 | self.description = description 34 | self.additionalProperties = additionalProperties 35 | } 36 | } 37 | 38 | /// A string schema 39 | case string(description: String? = nil, format: String? = nil) 40 | 41 | /// A number schema 42 | case number(description: String? = nil) 43 | 44 | /// A boolean schema 45 | case boolean(description: String? = nil) 46 | 47 | /// An array schema 48 | case array(items: JSONSchema, description: String? = nil) 49 | 50 | /// An object schema 51 | case object(Object) 52 | 53 | /// An enum schema with possible values 54 | case `enum`(values: [String], description: String? = nil) 55 | } 56 | 57 | // Extension to remove required fields from a schema 58 | extension JSONSchema { 59 | /// Returns a new schema with all required fields removed 60 | public var withoutRequired: JSONSchema { 61 | switch self { 62 | case .object(let object): 63 | // For object schemas, create a new object with empty required array 64 | return .object(Object(properties: object.properties.mapValues { $0.withoutRequired }, 65 | required: [], 66 | description: object.description, 67 | additionalProperties: object.additionalProperties)) 68 | 69 | case .array(let items, let description): 70 | // For array schemas, recursively apply to items 71 | return .array(items: items.withoutRequired, description: description) 72 | 73 | // For other schema types, return as is since they don't have required fields 74 | case .string, .number, .boolean, .enum: 75 | return self 76 | } 77 | } 78 | } 79 | 80 | // Extension to add additionalProperties:false to all objects, for use with structured results 81 | extension JSONSchema { 82 | /// Returns a new schema with all required fields removed 83 | public var addingAdditionalPropertiesRestrictionToObjects: JSONSchema { 84 | switch self { 85 | case .object(let object): 86 | return .object(Object(properties: object.properties.mapValues { $0.addingAdditionalPropertiesRestrictionToObjects }, 87 | required: object.required, 88 | description: object.description, 89 | additionalProperties: false)) 90 | 91 | case .array(let items, let description): 92 | // For array schemas, recursively apply to items 93 | return .array(items: items.addingAdditionalPropertiesRestrictionToObjects, description: description) 94 | 95 | // For other schema types, return as is since they don't have required fields 96 | default: 97 | return self 98 | } 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/MCPFunctionMetadata.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Common metadata about a function (tool or resource) 4 | public struct MCPFunctionMetadata: Sendable { 5 | /// The name of the function 6 | public let name: String 7 | 8 | /// A description of the function's purpose 9 | public let description: String? 10 | 11 | /// The parameters of the function 12 | public let parameters: [MCPParameterInfo] 13 | 14 | /// The return type of the function, if any 15 | public let returnType: Sendable.Type? 16 | 17 | /// A description of what the function returns 18 | public let returnTypeDescription: String? 19 | 20 | /// Whether the function is asynchronous 21 | public let isAsync: Bool 22 | 23 | /// Whether the function can throw errors 24 | public let isThrowing: Bool 25 | 26 | /** 27 | Creates a new MCPFunctionMetadata instance. 28 | 29 | - Parameters: 30 | - name: The name of the function 31 | - description: A description of the function's purpose 32 | - parameters: The parameters of the function 33 | - returnType: The return type of the function, if any 34 | - returnTypeDescription: A description of what the function returns 35 | - isAsync: Whether the function is asynchronous 36 | - isThrowing: Whether the function can throw errors 37 | */ 38 | public init( 39 | name: String, 40 | description: String? = nil, 41 | parameters: [MCPParameterInfo], 42 | returnType: Sendable.Type? = nil, 43 | returnTypeDescription: String? = nil, 44 | isAsync: Bool = false, 45 | isThrowing: Bool = false 46 | ) { 47 | self.name = name 48 | self.description = description 49 | self.parameters = parameters 50 | self.returnType = returnType 51 | self.returnTypeDescription = returnTypeDescription 52 | self.isAsync = isAsync 53 | self.isThrowing = isThrowing 54 | } 55 | 56 | /// Enriches a dictionary of arguments with default values and throws if a required parameter is missing 57 | public func enrichArguments(_ arguments: [String: Sendable]) throws -> [String: Sendable] { 58 | var enrichedArguments = arguments 59 | for param in parameters { 60 | if enrichedArguments[param.name] == nil { 61 | if let defaultValue = param.defaultValue { 62 | enrichedArguments[param.name] = defaultValue 63 | } else if param.isRequired { 64 | throw MCPToolError.missingRequiredParameter(parameterName: param.name) 65 | } 66 | } 67 | } 68 | return enrichedArguments 69 | } 70 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/MCPParameterInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Information about a function parameter 4 | public struct MCPParameterInfo: Sendable { 5 | /// The name of the parameter 6 | public let name: String 7 | 8 | /// The type of the parameter 9 | public let type: Sendable.Type 10 | 11 | /// An optional description of the parameter 12 | public let description: String? 13 | 14 | /// An optional default value for the parameter 15 | public let defaultValue: Sendable? 16 | 17 | /// Whether the parameter is required (no default value) 18 | public let isRequired: Bool 19 | 20 | /// Whether the parameter is optional (has a default value) 21 | public var isOptional: Bool { 22 | return !isRequired 23 | } 24 | 25 | /** 26 | Creates a new parameter info with the specified name, type, description, and default value. 27 | 28 | - Parameters: 29 | - name: The name of the parameter 30 | - type: The type of the parameter 31 | - description: An optional description of the parameter 32 | - defaultValue: An optional default value for the parameter 33 | - isRequired: Whether the parameter is required (no default value) 34 | */ 35 | public init(name: String, type: Sendable.Type, description: String? = nil, defaultValue: Sendable? = nil, isRequired: Bool) { 36 | self.name = name 37 | self.type = type 38 | self.description = description 39 | self.defaultValue = defaultValue 40 | self.isRequired = isRequired 41 | } 42 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Prompts/MCPPromptMetadata.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct MCPPromptMetadata: Sendable { 4 | public let functionMetadata: MCPFunctionMetadata 5 | 6 | public init( 7 | name: String, 8 | description: String? = nil, 9 | parameters: [MCPParameterInfo], 10 | isAsync: Bool = false, 11 | isThrowing: Bool = false 12 | ) { 13 | self.functionMetadata = MCPFunctionMetadata( 14 | name: name, 15 | description: description, 16 | parameters: parameters, 17 | returnType: nil, 18 | returnTypeDescription: nil, 19 | isAsync: isAsync, 20 | isThrowing: isThrowing 21 | ) 22 | } 23 | 24 | public var name: String { functionMetadata.name } 25 | public var description: String? { functionMetadata.description } 26 | public var parameters: [MCPParameterInfo] { functionMetadata.parameters } 27 | public var isAsync: Bool { functionMetadata.isAsync } 28 | public var isThrowing: Bool { functionMetadata.isThrowing } 29 | 30 | public func enrichArguments(_ arguments: [String: Sendable]) throws -> [String: Sendable] { 31 | return try functionMetadata.enrichArguments(arguments) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Prompts/Prompt.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AnyCodable 3 | 4 | /// Represents a prompt that can be provided by an MCP server 5 | public struct Prompt: Encodable, Sendable { 6 | /// Unique name of the prompt 7 | public let name: String 8 | 9 | /// Optional description of the prompt 10 | public let description: String? 11 | 12 | /// Arguments that the prompt accepts 13 | public let arguments: [MCPParameterInfo] 14 | 15 | public init(name: String, description: String? = nil, arguments: [MCPParameterInfo] = []) { 16 | self.name = name 17 | self.description = description 18 | self.arguments = arguments 19 | } 20 | } 21 | 22 | extension Prompt { 23 | private enum CodingKeys: String, CodingKey { 24 | case name, description, arguments 25 | } 26 | 27 | public func encode(to encoder: Encoder) throws { 28 | var container = encoder.container(keyedBy: CodingKeys.self) 29 | try container.encode(name, forKey: .name) 30 | try container.encodeIfPresent(description, forKey: .description) 31 | let args: [[String: AnyCodable]] = arguments.map { param in 32 | [ 33 | "name": AnyCodable(param.name), 34 | "description": AnyCodable(param.description ?? ""), 35 | "required": AnyCodable(param.isRequired) 36 | ] 37 | } 38 | try container.encode(args, forKey: .arguments) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Prompts/PromptMessage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct PromptMessage: Codable, Sendable { 4 | public enum Role: String, Codable, Sendable { 5 | case user 6 | case assistant 7 | } 8 | 9 | public struct Content: Codable, Sendable { 10 | public enum ContentType: String, Codable, Sendable { 11 | case text 12 | case image 13 | case audio 14 | case resource 15 | } 16 | 17 | public var type: ContentType 18 | public var text: String? 19 | public var data: Data? 20 | public var mimeType: String? 21 | public var resource: GenericResourceContent? 22 | 23 | public init(text: String) { 24 | self.type = .text 25 | self.text = text 26 | } 27 | 28 | public init(imageData: Data, mimeType: String) { 29 | self.type = .image 30 | self.data = imageData 31 | self.mimeType = mimeType 32 | } 33 | 34 | public init(audioData: Data, mimeType: String) { 35 | self.type = .audio 36 | self.data = audioData 37 | self.mimeType = mimeType 38 | } 39 | 40 | public init(resource: GenericResourceContent) { 41 | self.type = .resource 42 | self.resource = resource 43 | } 44 | } 45 | 46 | public var role: Role 47 | public var content: Content 48 | 49 | public init(role: Role, content: Content) { 50 | self.role = role 51 | self.content = content 52 | } 53 | 54 | /// Converts any result to an array of PromptMessage, similar to fastmcp Message logic 55 | public static func fromResult(_ result: Any) -> [PromptMessage] { 56 | if let message = result as? PromptMessage { 57 | return [message] 58 | } else if let messages = result as? [PromptMessage] { 59 | return messages 60 | } else if let str = result as? String { 61 | return [PromptMessage(role: .user, content: .init(text: str))] 62 | } else if let encodable = result as? Encodable { 63 | let encoder = JSONEncoder() 64 | encoder.outputFormatting = .prettyPrinted 65 | if let data = try? encoder.encode(AnyEncodable(encodable)), 66 | let json = String(data: data, encoding: .utf8) { 67 | return [PromptMessage(role: .user, content: .init(text: json))] 68 | } 69 | } 70 | // Fallback: use String(describing:) 71 | let text = String(describing: result) 72 | return [PromptMessage(role: .user, content: .init(text: text))] 73 | } 74 | } 75 | 76 | /// Helper to encode any Encodable type 77 | private struct AnyEncodable: Encodable { 78 | let value: Encodable 79 | init(_ value: Encodable) { self.value = value } 80 | func encode(to encoder: Encoder) throws { 81 | try value.encode(to: encoder) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Resources/GenericResourceContent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Generic implementation of MCPResourceContent for simple text resources 4 | public struct GenericResourceContent: MCPResourceContent { 5 | public let uri: URL 6 | public let mimeType: String? 7 | public let text: String? 8 | public let blob: Data? 9 | 10 | public init(uri: URL, mimeType: String? = nil, text: String? = nil, blob: Data? = nil) { 11 | self.uri = uri 12 | self.mimeType = mimeType 13 | self.text = text 14 | self.blob = blob 15 | } 16 | 17 | /// Converts any resource result to an array of MCPResourceContent 18 | public static func fromResult(_ result: Any, uri: URL, mimeType: String?) -> [MCPResourceContent] { 19 | // If already MCPResourceContent 20 | if let resourceContent = result as? MCPResourceContent { 21 | return [resourceContent] 22 | } else if let resourceArray = result as? [MCPResourceContent] { 23 | return resourceArray 24 | } else if type(of: result) == Void.self { 25 | return [] 26 | } else if let str = result as? String { 27 | return [GenericResourceContent(uri: uri, mimeType: mimeType ?? "text/plain", text: str)] 28 | } else if let boolVal = result as? Bool { 29 | return [GenericResourceContent(uri: uri, mimeType: mimeType ?? "text/plain", text: String(boolVal))] 30 | } else if let intVal = result as? Int { 31 | return [GenericResourceContent(uri: uri, mimeType: mimeType ?? "text/plain", text: String(intVal))] 32 | } else if let doubleVal = result as? Double { 33 | return [GenericResourceContent(uri: uri, mimeType: mimeType ?? "text/plain", text: String(doubleVal))] 34 | } else if let arr = result as? [Any] { 35 | let text = String(describing: arr) 36 | return [GenericResourceContent(uri: uri, mimeType: mimeType ?? "text/plain", text: text)] 37 | } else if let dict = result as? [String: Any] { 38 | let text = String(describing: dict) 39 | return [GenericResourceContent(uri: uri, mimeType: mimeType ?? "text/plain", text: text)] 40 | } else if let encodable = result as? Encodable { 41 | // Try to encode to JSON string 42 | let encoder = JSONEncoder() 43 | encoder.outputFormatting = .prettyPrinted 44 | if let data = try? encoder.encode(AnyEncodable(encodable)), 45 | let json = String(data: data, encoding: .utf8) { 46 | return [GenericResourceContent(uri: uri, mimeType: mimeType ?? "application/json", text: json)] 47 | } 48 | } 49 | // Fallback: use String(describing:) 50 | let text = String(describing: result) 51 | return [GenericResourceContent(uri: uri, mimeType: mimeType ?? "text/plain", text: text)] 52 | } 53 | } 54 | 55 | 56 | 57 | /// Helper to encode any Encodable type 58 | private struct AnyEncodable: Encodable { 59 | let value: Encodable 60 | init(_ value: Encodable) { self.value = value } 61 | func encode(to encoder: Encoder) throws { 62 | try value.encode(to: encoder) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Resources/MCPResource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Protocol defining the requirements for an MCP resource 4 | public protocol MCPResource: Codable, Sendable { 5 | /// The URI of the resource 6 | var uri: URL { get } 7 | 8 | /// The name of the resource 9 | var name: String { get } 10 | 11 | /// The description of the resource 12 | var description: String { get } 13 | 14 | /// The MIME type of the resource 15 | var mimeType: String { get } 16 | } 17 | 18 | /// Simple implementation of MCPResource 19 | public struct SimpleResource: MCPResource { 20 | public let uri: URL 21 | public let name: String 22 | public let description: String 23 | public let mimeType: String 24 | 25 | public init(uri: URL, name: String, description: String? = nil, mimeType: String? = nil) { 26 | self.uri = uri 27 | self.name = name 28 | self.description = description ?? "" 29 | self.mimeType = mimeType ?? "text/plain" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Resources/MCPResourceContent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Protocol defining the requirements for MCP resource content 4 | public protocol MCPResourceContent: Codable, Sendable { 5 | /// The URI of the resource 6 | var uri: URL { get } 7 | 8 | /// The MIME type of the resource (optional) 9 | var mimeType: String? { get } 10 | 11 | /// The text content of the resource (if it's a text resource) 12 | var text: String? { get } 13 | 14 | /// The binary content of the resource (if it's a binary resource) 15 | var blob: Data? { get } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Resources/MCPResourceKind.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPResourceKind.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 03.04.25. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents the kind of content a resource can provide. 12 | 13 | Resources can provide either textual or binary data: 14 | - text: Plain text content 15 | - data: Binary data content 16 | */ 17 | public enum MCPResourceKind 18 | { 19 | case text(String) 20 | 21 | case data(Data) 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Resources/MCPResourceMetadata.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Metadata about a resource function 4 | public struct MCPResourceMetadata: Sendable { 5 | /// The common function metadata 6 | public let functionMetadata: MCPFunctionMetadata 7 | 8 | /// The URI templates of the resource 9 | public let uriTemplates: Set 10 | 11 | /// The display name of the resource 12 | public let name: String 13 | 14 | /// The MIME type of the resource (optional) 15 | public let mimeType: String? 16 | 17 | /** 18 | Creates a new MCPResourceMetadata instance. 19 | 20 | - Parameters: 21 | - uriTemplates: The URI templates of the resource 22 | - name: The display name of the resource (overrides function name if different) 23 | - functionName: The name of the function (for dispatching) 24 | - description: A description of the resource 25 | - parameters: The parameters of the function 26 | - returnType: The return type of the function, if any 27 | - returnTypeDescription: A description of what the function returns 28 | - isAsync: Whether the function is asynchronous 29 | - isThrowing: Whether the function can throw errors 30 | - mimeType: The MIME type of the resource 31 | */ 32 | public init( 33 | uriTemplates: Set, 34 | name: String? = nil, 35 | functionName: String, 36 | description: String? = nil, 37 | parameters: [MCPParameterInfo], 38 | returnType: Sendable.Type? = nil, 39 | returnTypeDescription: String? = nil, 40 | isAsync: Bool = false, 41 | isThrowing: Bool = false, 42 | mimeType: String? = nil 43 | ) { 44 | self.name = name ?? functionName 45 | self.functionMetadata = MCPFunctionMetadata( 46 | name: functionName, 47 | description: description, 48 | parameters: parameters, 49 | returnType: returnType, 50 | returnTypeDescription: returnTypeDescription, 51 | isAsync: isAsync, 52 | isThrowing: isThrowing 53 | ) 54 | self.uriTemplates = uriTemplates 55 | self.mimeType = mimeType 56 | } 57 | 58 | // Convenience accessors for common properties 59 | public var description: String? { functionMetadata.description } 60 | public var parameters: [MCPParameterInfo] { functionMetadata.parameters } 61 | public var returnType: Sendable.Type? { functionMetadata.returnType } 62 | public var returnTypeDescription: String? { functionMetadata.returnTypeDescription } 63 | public var isAsync: Bool { functionMetadata.isAsync } 64 | public var isThrowing: Bool { functionMetadata.isThrowing } 65 | 66 | /// Converts metadata to MCPResourceTemplate array (one for each URI template) 67 | public func toResourceTemplates() -> [SimpleResourceTemplate] { 68 | return uriTemplates.map { template in 69 | SimpleResourceTemplate( 70 | uriTemplate: template, 71 | name: name, 72 | description: description, 73 | mimeType: mimeType 74 | ) 75 | } 76 | } 77 | 78 | /// Enriches a dictionary of arguments with default values and throws if a required parameter is missing 79 | public func enrichArguments(_ arguments: [String: Sendable]) throws -> [String: Sendable] { 80 | return try functionMetadata.enrichArguments(arguments) 81 | } 82 | 83 | /// Finds the best matching URI template for a given URL 84 | /// Returns the template that matches the most parameters 85 | public func bestMatchingTemplate(for url: URL) -> String? { 86 | var bestTemplate: String? 87 | var maxParameterCount = -1 88 | 89 | for template in uriTemplates { 90 | if let variables = url.extractTemplateVariables(from: template) { 91 | let parameterCount = variables.count 92 | if parameterCount > maxParameterCount { 93 | maxParameterCount = parameterCount 94 | bestTemplate = template 95 | } 96 | } 97 | } 98 | 99 | return bestTemplate 100 | } 101 | } 102 | 103 | /// Simple implementation of MCPResourceTemplate 104 | public struct SimpleResourceTemplate: MCPResourceTemplate { 105 | public let uriTemplate: String 106 | public let name: String 107 | public let description: String? 108 | public let mimeType: String? 109 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Resources/MCPResourceTemplate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Protocol defining the requirements for an MCP resource template 4 | public protocol MCPResourceTemplate: Codable, Sendable { 5 | /// The URI template of the resource 6 | var uriTemplate: String { get } 7 | 8 | /// The name of the resource 9 | var name: String { get } 10 | 11 | /// The description of the resource 12 | var description: String? { get } 13 | 14 | /// The MIME type of the resource 15 | var mimeType: String? { get } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Schema/SchemaMetadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPToolMetadata.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 30.03.25. 6 | // 7 | 8 | 9 | import Foundation 10 | 11 | /// Metadata about a SchemaRepresentable struct 12 | public struct SchemaMetadata: Sendable { 13 | /// The name of the type 14 | public let name: String 15 | 16 | /// The parameters of the function 17 | public let parameters: [SchemaPropertyInfo] 18 | 19 | /// A description of the function's purpose 20 | public let description: String? 21 | 22 | /** 23 | Creates a new MCPToolMetadata instance. 24 | 25 | - Parameters: 26 | - name: The name of the function 27 | - description: A description of the function's purpose 28 | - parameters: The parameters of the function 29 | */ 30 | public init(name: String, description: String? = nil, parameters: [SchemaPropertyInfo]) { 31 | self.name = name 32 | self.description = description 33 | self.parameters = parameters 34 | } 35 | 36 | /// Converts this schema metadata to a JSON Schema representation 37 | public var schema: JSONSchema { 38 | // Convert parameters to properties 39 | var properties: [String: JSONSchema] = [:] 40 | var required: [String] = [] 41 | 42 | for param in parameters { 43 | let schema = param.schema 44 | properties[param.name] = schema 45 | 46 | if param.isRequired { 47 | required.append(param.name) 48 | } 49 | } 50 | 51 | return .object(JSONSchema.Object( 52 | properties: properties, 53 | required: required, 54 | description: description 55 | )) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Schema/SchemaPropertyInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemaPropertyInfo.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 30.03.25. 6 | // 7 | 8 | 9 | import Foundation 10 | 11 | /// Information about a function parameter 12 | public struct SchemaPropertyInfo: Sendable { 13 | /// The name of the parameter 14 | public let name: String 15 | 16 | /// The actual type of the parameter (e.g. Address.self) 17 | public let type: Any.Type 18 | 19 | /// An optional description of the parameter 20 | public let description: String? 21 | 22 | /// An optional default value for the parameter 23 | public let defaultValue: Sendable? 24 | 25 | /// Whether the parameter is required (no default value) 26 | public let isRequired: Bool 27 | 28 | /** 29 | Creates a new parameter info with the specified name, type, description, and default value. 30 | 31 | - Parameters: 32 | - name: The name of the parameter 33 | - schemaType: The actual type of the parameter (e.g. Address.self) 34 | - description: An optional description of the parameter 35 | - defaultValue: An optional default value for the parameter 36 | */ 37 | public init(name: String, type: Any.Type, description: String? = nil, defaultValue: Sendable? = nil, isRequired: Bool) { 38 | self.name = name 39 | self.type = type 40 | self.description = description 41 | self.defaultValue = defaultValue 42 | self.isRequired = isRequired 43 | } 44 | 45 | /// Converts this property info to a JSON Schema representation 46 | public var schema: JSONSchema { 47 | // If this is a JSONSchemaTypeConvertible type, use its schema 48 | if let convertibleType = type as? any JSONSchemaTypeConvertible.Type { 49 | return convertibleType.jsonSchema(description: description) 50 | } 51 | 52 | // If this is a SchemaRepresentable type, use its schema 53 | if let schemaType = type as? any SchemaRepresentable.Type { 54 | return schemaType.schemaMetadata.schema 55 | } 56 | 57 | // If this is a CaseIterable type that isn't JSONSchemaTypeConvertible, return a string schema with enum values 58 | if let caseIterableType = type as? any CaseIterable.Type { 59 | return JSONSchema.enum(values: caseIterableType.caseLabels, description: description ) 60 | } 61 | 62 | // Default to string for unknown types 63 | return JSONSchema.string(description: description) 64 | } 65 | 66 | public var jsonSchema: JSONSchema { 67 | return schema 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Schema/SchemaRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemaRepresentable.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 30.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Protocol for types that can represent themselves as a JSON Schema 11 | public protocol SchemaRepresentable: Sendable { 12 | 13 | /// The metadata for the schema 14 | static var schemaMetadata: SchemaMetadata { get } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Tools/MCPTool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPTool.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 08.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents a tool that can be used by an AI model 11 | public struct MCPTool: Sendable { 12 | /// The name of the tool 13 | public let name: String 14 | 15 | /// An optional description of the tool 16 | public let description: String? 17 | 18 | /// The JSON schema defining the tool's input parameters 19 | public let inputSchema: JSONSchema 20 | 21 | /** 22 | Creates a new tool with the specified name, description, and input schema. 23 | 24 | - Parameters: 25 | - name: The name of the tool 26 | - description: An optional description of the tool 27 | - inputSchema: The schema defining the function's input parameters 28 | */ 29 | public init(name: String, description: String? = nil, inputSchema: JSONSchema) { 30 | self.name = name 31 | self.description = description 32 | self.inputSchema = inputSchema 33 | } 34 | } 35 | 36 | /** 37 | Extension to make MCPTool conform to Codable 38 | */ 39 | extension MCPTool: Codable { 40 | // MARK: - Codable Implementation 41 | 42 | private enum CodingKeys: String, CodingKey { 43 | case name 44 | case description 45 | case inputSchema 46 | } 47 | 48 | public init(from decoder: Decoder) throws { 49 | let container = try decoder.container(keyedBy: CodingKeys.self) 50 | let name = try container.decode(String.self, forKey: .name) 51 | let description = try container.decodeIfPresent(String.self, forKey: .description) 52 | let inputSchema = try container.decode(JSONSchema.self, forKey: .inputSchema) 53 | 54 | self.init(name: name, description: description, inputSchema: inputSchema) 55 | } 56 | 57 | public func encode(to encoder: Encoder) throws { 58 | var container = encoder.container(keyedBy: CodingKeys.self) 59 | try container.encode(name, forKey: .name) 60 | try container.encodeIfPresent(description, forKey: .description) 61 | try container.encode(inputSchema, forKey: .inputSchema) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Tools/MCPToolError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Errors that can occur when calling a tool 4 | public enum MCPToolError: Error { 5 | /// The tool with the given name doesn't exist 6 | case unknownTool(name: String) 7 | 8 | /// An argument couldn't be cast to the correct type 9 | case invalidArgumentType(parameterName: String, expectedType: String, actualType: String) 10 | 11 | /// An argument couldn't be cast to the correct type 12 | case invalidEnumValue(parameterName: String, expectedValues: [String], actualValue: String) 13 | 14 | /// The input is not a valid JSON dictionary 15 | case invalidJSONDictionary 16 | 17 | /// A required parameter is missing 18 | case missingRequiredParameter(parameterName: String) 19 | } 20 | 21 | extension MCPToolError: LocalizedError { 22 | public var errorDescription: String? { 23 | switch self { 24 | case .unknownTool(let name): 25 | return "The tool '\(name)' was not found on the server" 26 | case .invalidArgumentType(let parameterName, let expectedType, let actualType): 27 | return "Parameter '\(parameterName)' expected type \(expectedType) but received type \(actualType)" 28 | case .invalidEnumValue(let parameterName, let expectedValues, let actualValue): 29 | let string = expectedValues.joined(separator: ", ") 30 | return "Parameter '\(parameterName)' expected one of [\(string)] but received \(actualValue)" 31 | case .invalidJSONDictionary: 32 | return "The input could not be parsed as a valid JSON dictionary" 33 | case .missingRequiredParameter(let parameterName): 34 | return "Missing required parameter '\(parameterName)'" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Models/Tools/MCPToolMetadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPToolMetadata.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 08.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Metadata about a tool function 11 | public struct MCPToolMetadata: Sendable { 12 | /// The common function metadata 13 | public let functionMetadata: MCPFunctionMetadata 14 | 15 | /// Whether the function's actions are consequential (defaults to true) 16 | public let isConsequential: Bool 17 | 18 | /** 19 | Creates a new MCPToolMetadata instance. 20 | 21 | - Parameters: 22 | - name: The name of the function 23 | - description: A description of the function's purpose 24 | - parameters: The parameters of the function 25 | - returnType: The return type of the function, if any 26 | - returnTypeDescription: A description of what the function returns 27 | - isAsync: Whether the function is asynchronous 28 | - isThrowing: Whether the function can throw errors 29 | - isConsequential: Whether the function's actions are consequential 30 | */ 31 | public init( 32 | name: String, 33 | description: String? = nil, 34 | parameters: [MCPParameterInfo], 35 | returnType: Sendable.Type? = nil, 36 | returnTypeDescription: String? = nil, 37 | isAsync: Bool = false, 38 | isThrowing: Bool = false, 39 | isConsequential: Bool = true 40 | ) { 41 | self.functionMetadata = MCPFunctionMetadata( 42 | name: name, 43 | description: description, 44 | parameters: parameters, 45 | returnType: returnType, 46 | returnTypeDescription: returnTypeDescription, 47 | isAsync: isAsync, 48 | isThrowing: isThrowing 49 | ) 50 | self.isConsequential = isConsequential 51 | } 52 | 53 | // Convenience accessors for common properties 54 | public var name: String { functionMetadata.name } 55 | public var description: String? { functionMetadata.description } 56 | public var parameters: [MCPParameterInfo] { functionMetadata.parameters } 57 | public var returnType: Sendable.Type? { functionMetadata.returnType } 58 | public var returnTypeDescription: String? { functionMetadata.returnTypeDescription } 59 | public var isAsync: Bool { functionMetadata.isAsync } 60 | public var isThrowing: Bool { functionMetadata.isThrowing } 61 | 62 | /// Enriches a dictionary of arguments with default values and throws if a required parameter is missing 63 | public func enrichArguments(_ arguments: [String: Sendable]) throws -> [String: Sendable] { 64 | return try functionMetadata.enrichArguments(arguments) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/OpenAPI/AIPluginManifest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Represents the AI plugin manifest structure 4 | struct AIPluginManifest: Codable { 5 | /// The schema version of the manifest 6 | let schemaVersion: String = "v1" 7 | 8 | /// The human-readable name of the plugin 9 | let nameForHuman: String 10 | 11 | /// The model-readable name of the plugin 12 | let nameForModel: String 13 | 14 | /// The human-readable description of the plugin 15 | let descriptionForHuman: String 16 | 17 | /// The model-readable description of the plugin 18 | let descriptionForModel: String 19 | 20 | /// The authentication configuration 21 | let auth: Auth 22 | 23 | /// The API configuration 24 | let api: API 25 | 26 | /// Coding keys for JSON serialization 27 | enum CodingKeys: String, CodingKey { 28 | case schemaVersion = "schema_version" 29 | case nameForHuman = "name_for_human" 30 | case nameForModel = "name_for_model" 31 | case descriptionForHuman = "description_for_human" 32 | case descriptionForModel = "description_for_model" 33 | case auth 34 | case api 35 | } 36 | 37 | /// Authentication configuration 38 | struct Auth: Codable { 39 | /// The type of authentication 40 | let type: AuthType 41 | 42 | /// The type of authorization (only for user_http) 43 | let authorizationType: String? 44 | 45 | /// Instructions for authentication (only for user_http) 46 | let instructions: String? 47 | 48 | enum CodingKeys: String, CodingKey { 49 | case type 50 | case authorizationType = "authorization_type" 51 | case instructions 52 | } 53 | 54 | /// Create an auth configuration for no authentication 55 | static var none: Auth { 56 | Auth(type: .none, authorizationType: nil, instructions: nil) 57 | } 58 | 59 | /// Create an auth configuration for bearer token authentication 60 | static var bearer: Auth { 61 | Auth( 62 | type: .userHttp, 63 | authorizationType: "bearer", 64 | instructions: "Enter your Bearer Token to authenticate with the API." 65 | ) 66 | } 67 | } 68 | 69 | /// Authentication types supported by the manifest 70 | enum AuthType: String, Codable { 71 | case none 72 | case userHttp = "user_http" 73 | } 74 | 75 | /// API configuration 76 | struct API: Codable { 77 | /// The type of API 78 | let type: String 79 | 80 | /// The URL of the API specification 81 | let url: String 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/OpenAPI/FileContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileContent.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 06.04.25. 6 | // 7 | 8 | 9 | import Foundation 10 | 11 | /// Contents of a File to be returned by a tool call 12 | @Schema 13 | public struct FileContent: Codable, Sendable { 14 | /// The name of the file 15 | public let name: String 16 | 17 | /// The MIME type of the file 18 | public let mimeType: String 19 | 20 | /** 21 | The content of the file in base64 encoding 22 | */ 23 | public let content: Data 24 | 25 | /** 26 | Creates a new file response 27 | 28 | - Parameters: 29 | - name: The name of the file 30 | - mimeType: The MIME type of the file 31 | - content: The content of the file in base64 encoding 32 | */ 33 | public init(name: String, mimeType: String, content: Data) { 34 | self.name = name 35 | self.mimeType = mimeType 36 | self.content = content 37 | } 38 | 39 | private enum CodingKeys: String, CodingKey { 40 | case name 41 | case mimeType = "mime_type" 42 | case content 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/OpenAPI/OpenAIFileResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenAIFileResponse.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 08.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// One or more files being returned 11 | @Schema 12 | public struct OpenAIFileResponse: Codable, Sendable { 13 | /// The array of file responses 14 | public let openaiFileResponse: [FileContent] 15 | 16 | /** 17 | Creates a new collection of file responses 18 | 19 | - Parameter files: The array of file responses 20 | */ 21 | public init(files: [FileContent]) { 22 | self.openaiFileResponse = files 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Protocols/MCPCompletionProviding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPCompletionProviding.swift 3 | // SwiftMCP 4 | // 5 | // Created by Codex. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Describes the context for which completion values should be provided. 11 | public enum MCPCompletionContext: Sendable { 12 | case resource(MCPResourceMetadata) 13 | case prompt(MCPPromptMetadata) 14 | } 15 | 16 | /// Protocol for providing completions for argument values. 17 | public protocol MCPCompletionProviding: MCPService { 18 | /// Returns completion values for the given parameter in the provided context. 19 | /// - Parameters: 20 | /// - parameter: The parameter for which a completion is requested. 21 | /// - context: The prompt or resource context. 22 | /// - prefix: The prefix string already entered by the client. 23 | func completion(for parameter: MCPParameterInfo, in context: MCPCompletionContext, prefix: String) async -> CompleteResult.Completion 24 | } 25 | 26 | public extension MCPCompletionProviding { 27 | /// Default implementation that mirrors the behaviour of `MCPParameterInfo.defaultCompletion(prefix:)`. 28 | func completion(for parameter: MCPParameterInfo, in context: MCPCompletionContext, prefix: String) async -> CompleteResult.Completion { 29 | return parameter.defaultCompletion(prefix: prefix) 30 | } 31 | 32 | /// Provides completion values for `CaseIterable` enums. 33 | func defaultEnumCompletion(for parameter: MCPParameterInfo, prefix: String) -> CompleteResult.Completion? { 34 | return parameter.defaultEnumCompletion(prefix: prefix) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Protocols/MCPPromptProviding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Protocol for servers that provide prompts to clients 4 | public protocol MCPPromptProviding { 5 | /// Metadata for all prompt functions 6 | nonisolated var mcpPromptMetadata: [MCPPromptMetadata] { get } 7 | 8 | /// Calls a prompt by name with provided arguments 9 | func callPrompt(_ name: String, arguments: [String: Sendable]) async throws -> [PromptMessage] 10 | } 11 | 12 | extension MCPPromptProviding { 13 | public var mcpPromptMetadata: [MCPPromptMetadata] { [] } 14 | 15 | public func callPrompt(_ name: String, arguments: [String: Sendable]) async throws -> [PromptMessage] { 16 | throw MCPToolError.unknownTool(name: name) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Protocols/MCPResourceProviding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPResourceProviding.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 03.04.25. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol MCPResourceProviding { 11 | /** 12 | The resources available on this server. 13 | 14 | Resources are data objects that can be accessed through the MCP protocol. 15 | Each resource has a URI, name, description, and MIME type. 16 | */ 17 | var mcpResources: [MCPResource] { get async } 18 | 19 | /** 20 | The resource templates available on this server. 21 | 22 | Resource templates define patterns for resources that can be dynamically created 23 | or accessed. Each template has a URI pattern, name, description, and MIME type. 24 | */ 25 | var mcpResourceTemplates: [MCPResourceTemplate] { get async } 26 | 27 | /** 28 | The resource metadata available on this server. 29 | 30 | Resource metadata contains information about resource functions including their 31 | parameters, return types, and other metadata needed for OpenAPI generation. 32 | */ 33 | var mcpResourceMetadata: [MCPResourceMetadata] { get } 34 | 35 | /** 36 | Retrieves a resource by its URI. 37 | 38 | - Parameter uri: The URI of the resource to retrieve 39 | - Returns: The resource content if found, nil otherwise 40 | - Throws: An error if the resource cannot be accessed 41 | */ 42 | func getResource(uri: URL) async throws -> [MCPResourceContent] 43 | 44 | /** 45 | Calls a resource function by name with the provided arguments (for OpenAPI support). 46 | 47 | - Parameters: 48 | - name: The name of the resource function to call 49 | - arguments: The arguments to pass to the resource function 50 | - Returns: The result of the resource function execution 51 | - Throws: An error if the resource function doesn't exist or cannot be called 52 | */ 53 | func callResourceAsFunction(_ name: String, arguments: [String: Sendable]) async throws -> Encodable & Sendable 54 | 55 | /** 56 | Handles non-template resources (e.g., file-based resources). 57 | Override this method to provide custom resource content handling. 58 | 59 | - Parameters: 60 | - uri: The URI of the resource 61 | - Returns: The resource content 62 | - Throws: An error if the resource cannot be accessed 63 | */ 64 | func getNonTemplateResource(uri: URL) async throws -> [MCPResourceContent] 65 | 66 | 67 | } 68 | 69 | 70 | extension MCPResourceProviding { 71 | 72 | /// Returns an array of all MCP resources defined in this type 73 | public var mcpResources: [any MCPResource] { 74 | get async { 75 | return [] 76 | } 77 | } 78 | 79 | /// Resource templates with zero parameters are listed together with mcpResources 80 | var mcpStaticResources: [MCPResource] 81 | { 82 | // Find the resources without parameters 83 | let mirror = Mirror(reflecting: self) 84 | 85 | let array: [MCPResourceMetadata] = mirror.children.compactMap { child in 86 | 87 | guard let label = child.label, 88 | label.hasPrefix("__mcpResourceMetadata_") else { 89 | return nil 90 | } 91 | 92 | guard let metadata = child.value as? MCPResourceMetadata else { 93 | return nil 94 | } 95 | 96 | guard metadata.parameters.isEmpty else 97 | { 98 | return nil 99 | } 100 | 101 | return metadata 102 | } 103 | 104 | // Create individual resources for each URI template 105 | return array.flatMap { metadata in 106 | metadata.uriTemplates.compactMap { template in 107 | guard let url = URL(string: template) else { return nil } 108 | return SimpleResource(uri: url, name: metadata.name, description: metadata.description, mimeType: metadata.mimeType) 109 | } 110 | } 111 | } 112 | 113 | /// Default implementation of mcpResourceMetadata - returns empty array 114 | /// This should be overridden by the MCPServerMacro 115 | public var mcpResourceMetadata: [MCPResourceMetadata] { 116 | return [] 117 | } 118 | 119 | public func callResourceAsFunction(_ name: String, arguments: [String: Sendable]) async throws -> Encodable & Sendable { 120 | // Default implementation throws an error - this should be overridden by the macro 121 | throw MCPResourceError.notFound(uri: "function://\(name)") 122 | } 123 | 124 | public func getNonTemplateResource(uri: URL) async throws -> [MCPResourceContent] { 125 | // Default implementation: returns an empty array, indicating no non-template resource found by default. 126 | // Implementers should override this to provide specific non-template resource handling. 127 | return [] 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Protocols/MCPService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPService.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 03.04.25. 6 | // 7 | 8 | /** 9 | Root protocol for MCP service capabilities. 10 | 11 | This protocol serves as a marker interface and base type for all MCP service protocols. 12 | In the Model-Client Protocol (MCP) architecture, services represent discrete capabilities 13 | that can be provided by a server, such as tools, resources, or other functionalities. 14 | 15 | Services can be discovered at runtime through the `MCPServer` protocol's service discovery 16 | methods, allowing for flexible composition of capabilities across servers. 17 | 18 | By separating capabilities into distinct service protocols that inherit from `MCPService`, 19 | the architecture supports: 20 | - Better separation of concerns 21 | - Runtime service discovery and composition 22 | - Ability to implement only the services needed by a particular server 23 | - Hierarchical aggregation of services from multiple servers 24 | */ 25 | public protocol MCPService 26 | { 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Protocols/MCPToolProviding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPToolProviding.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 03.04.25. 6 | // 7 | 8 | /** 9 | Protocol defining a service that provides callable tools for MCP. 10 | 11 | The `MCPToolProviding` protocol is a core service in the MCP architecture that allows 12 | servers to expose functions as remotely callable tools. Tools are functions that: 13 | - Have well-defined parameters and return types 14 | - Can be discovered and called through the MCP protocol 15 | - Are typically decorated with the `@MCPTool` macro for automatic metadata generation 16 | 17 | Servers implementing this protocol must: 18 | 1. Provide a list of available tools via the `mcpTools` property 19 | 2. Implement the `callTool` method to execute a tool by name with provided arguments 20 | 21 | When used with the `@MCPToolProvider` macro, conformance to this protocol can be 22 | automatically generated based on functions decorated with `@MCPTool`. 23 | 24 | Tools are discovered at runtime and can be called remotely through JSON-RPC, 25 | enabling flexible interaction between AI models and server capabilities. 26 | */ 27 | public protocol MCPToolProviding: MCPService { 28 | /** 29 | Provides metadata for all functions annotated with `@MCPTool`. 30 | 31 | This property uses runtime reflection to gather tool metadata from properties 32 | generated by the `@MCPTool` macro. 33 | */ 34 | nonisolated var mcpToolMetadata: [MCPToolMetadata] { get } 35 | 36 | /** 37 | Calls a tool by name with the provided arguments. 38 | 39 | - Parameters: 40 | - name: The name of the tool to call 41 | - arguments: The arguments to pass to the tool 42 | - Returns: The result of the tool execution 43 | - Throws: An error if the tool execution fails 44 | */ 45 | func callTool(_ name: String, arguments: [String: Sendable]) async throws -> Encodable & Sendable 46 | } 47 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Articles/CoreConcepts.md: -------------------------------------------------------------------------------- 1 | # Core Concepts 2 | 3 | Learn how SwiftMCP uses documentation comments to power AI interactions. 4 | 5 | ## Overview 6 | 7 | SwiftMCP relies heavily on documentation comments to provide meaningful information about your server and its tools to AI assistants. This article explains how the framework extracts and uses this documentation. 8 | 9 | ## Documentation Comment Syntax 10 | 11 | SwiftMCP follows Swift's standard documentation comment syntax, which uses Markdown-flavored markup. The framework specifically looks for: 12 | 13 | - Main description in the comment body 14 | - Parameter descriptions using either format: 15 | ```swift 16 | - Parameters: 17 | - x: Description of x 18 | - y: Description of y 19 | ``` 20 | or 21 | ```swift 22 | - Parameter x: Description of x 23 | - Parameter y: Description of y 24 | ``` 25 | - Return value documentation: 26 | ```swift 27 | - Returns: Description of return value 28 | ``` 29 | 30 | You can also use other documentation extensions like: 31 | - `- Note:` 32 | - `- Important:` 33 | - `- Warning:` 34 | - `- Attention:` 35 | 36 | For complete details on Swift's documentation comment syntax, see the [official documentation](https://github.com/swiftlang/swift/blob/main/docs/DocumentationComments.md). 37 | 38 | ## Server Documentation 39 | 40 | When you create a server using the `@MCPServer` macro, it automatically extracts documentation from your class's comments: 41 | 42 | ```swift 43 | /** 44 | A calculator server that provides basic arithmetic operations. 45 | 46 | This server exposes mathematical functions like addition, subtraction, 47 | multiplication and division through a JSON-RPC interface. 48 | */ 49 | @MCPServer 50 | class Calculator { 51 | /// The name of the server, defaults to class name if not specified 52 | var serverName: String { "Calculator" } 53 | 54 | /// The version of the server, defaults to "1.0.0" if not specified 55 | var serverVersion: String { "2.0.0" } 56 | } 57 | ``` 58 | 59 | The macro uses: 60 | - The class's documentation comment as the server description 61 | - The `serverName` property to identify the server (optional) 62 | - The `serverVersion` property for versioning (optional) 63 | 64 | ## Tool Documentation 65 | 66 | Tools are methods marked with the `@MCPTool` macro. The macro extracts documentation from method comments: 67 | 68 | ```swift 69 | /** 70 | Adds two numbers and returns their sum. 71 | 72 | This function takes two integers as input and returns their arithmetic sum. 73 | Useful for basic addition operations. 74 | 75 | - Parameter a: The first number to add 76 | - Parameter b: The second number to add 77 | - Returns: The sum of a and b 78 | */ 79 | @MCPTool 80 | func add(a: Int, b: Int) -> Int { 81 | a + b 82 | } 83 | ``` 84 | 85 | The macro extracts: 86 | - The method's main comment as the tool description 87 | - Parameter documentation for each argument 88 | - Return value documentation 89 | 90 | ## Custom Descriptions 91 | 92 | You can override a tool's description using the `description` parameter of the `@MCPTool` macro: 93 | 94 | ```swift 95 | /// This description will be overridden 96 | @MCPTool(description: "Custom description for the tool") 97 | func customTool() { } 98 | ``` 99 | 100 | The custom description will take precedence over any documentation comments. 101 | 102 | ## Importance for AI Integration 103 | 104 | Documentation comments are crucial because: 105 | 1. They provide context to AI assistants about your server's purpose 106 | 2. They explain what each tool does and how to use it 107 | 3. They describe what parameters mean and what values are expected 108 | 4. They help AIs understand return values 109 | 110 | Without proper documentation: 111 | - AIs won't understand your server's purpose 112 | - Tools may be used incorrectly 113 | - Parameters may receive invalid values 114 | - Return values may be misinterpreted 115 | 116 | ## Best Practices 117 | 118 | 1. Always document your server class with: 119 | - Overall purpose 120 | - Key features 121 | - Usage examples 122 | 123 | 2. Document each tool with: 124 | - Clear description of functionality 125 | - Parameter descriptions 126 | - Return value explanation 127 | 128 | 3. Use markdown formatting in comments for better readability 129 | 130 | 4. Include examples in complex tool documentation 131 | 132 | 5. Document any constraints or requirements 133 | 134 | ## See Also 135 | 136 | - [Swift Documentation Comments Guide](https://github.com/swiftlang/swift/blob/main/docs/DocumentationComments.md) 137 | - 138 | - ``MCPServer`` 139 | - ``MCPTool`` -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Articles/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started with SwiftMCP 2 | 3 | Create your first MCP server in minutes. 4 | 5 | ## Overview 6 | 7 | SwiftMCP makes it easy to build Model Context Protocol (MCP) servers that can interact with AI models. This guide will help you get started with the basics. 8 | 9 | ## Installation 10 | 11 | Add SwiftMCP to your project using Swift Package Manager: 12 | 13 | ```swift 14 | dependencies: [ 15 | .package(url: "https://github.com/Cocoanetics/SwiftMCP.git", branch: "main") 16 | ] 17 | ``` 18 | 19 | ## Basic Usage 20 | 21 | 1. Create a new Swift file for your server: 22 | 23 | ```swift 24 | import SwiftMCP 25 | 26 | @MCPServer(version: "1.0.0") 27 | struct Calculator { 28 | @MCPTool(description: "Adds two numbers together") 29 | func add(a: Double, b: Double) -> Double { 30 | return a + b 31 | } 32 | } 33 | ``` 34 | 35 | 2. Run your server: 36 | 37 | ```swift 38 | import SwiftMCP 39 | 40 | let calculator = Calculator() 41 | let transport = StdioTransport() 42 | 43 | try await transport.start(server: calculator) 44 | ``` 45 | 46 | ## Next Steps 47 | 48 | - Follow the tutorial to learn more advanced features 49 | - Explore the API documentation for detailed information about available options 50 | - Check out the example projects in the repository -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Articles/SupportedTypes.md: -------------------------------------------------------------------------------- 1 | # Supported Types in MCP Tools 2 | 3 | Learn about the parameter types and function signatures supported by MCP tools. 4 | 5 | ## Overview 6 | 7 | When creating MCP tools using the `@MCPTool` macro, you can use various parameter types and function signatures. This article covers all supported types and shows examples of how to use them. 8 | 9 | ## Parameter Types 10 | 11 | MCP tools support the following parameter types: 12 | 13 | ### Basic Types 14 | - `Int` 15 | - `Double` 16 | - `Float` 17 | - `String` 18 | - `Bool` 19 | 20 | ### Array Types 21 | - `[Int]` 22 | - `[Double]` 23 | - `[Float]` 24 | - `[String]` 25 | - `[Bool]` 26 | 27 | ### Enum Types 28 | Enums without associated values that conform to `CaseIterable` and `Sendable` are supported. By default, the case labels are used as strings when the enum is serialized. You can customize the string representation by implementing `CustomStringConvertible`: 29 | 30 | ```swift 31 | // Default behavior uses case labels 32 | enum SearchOption: CaseIterable, Sendable { 33 | case all // Will be serialized as "all" 34 | case unread // Will be serialized as "unread" 35 | case flagged // Will be serialized as "flagged" 36 | } 37 | 38 | // Custom string representation using CustomStringConvertible 39 | enum FilterOption: CaseIterable, Sendable, CustomStringConvertible { 40 | case newest 41 | case oldest 42 | case popular 43 | 44 | var description: String { 45 | switch self { 46 | case .newest: return "SORT_NEW" 47 | case .oldest: return "SORT_OLD" 48 | case .popular: return "SORT_POPULAR" 49 | } 50 | } 51 | } 52 | 53 | // Raw values are ignored for serialization 54 | enum Priority: String, CaseIterable, Sendable { 55 | case high = "H" // Will be serialized as "high" 56 | case medium = "M" // Will be serialized as "medium" 57 | case low = "L" // Will be serialized as "low" 58 | } 59 | ``` 60 | 61 | ## Function Signatures 62 | 63 | MCP tools support various function signatures: 64 | 65 | ### Basic Functions 66 | ```swift 67 | @MCPTool 68 | func add(a: Int, b: Int) -> Int { 69 | return a + b 70 | } 71 | ``` 72 | 73 | ### Async Functions 74 | ```swift 75 | @MCPTool 76 | func fetchData(query: String) async -> String { 77 | // ... async implementation 78 | } 79 | ``` 80 | 81 | ### Throwing Functions 82 | ```swift 83 | @MCPTool 84 | func divide(numerator: Double, denominator: Double) throws -> Double { 85 | guard denominator != 0 else { 86 | throw MathError.divisionByZero 87 | } 88 | return numerator / denominator 89 | } 90 | ``` 91 | 92 | ### Async Throwing Functions 93 | ```swift 94 | @MCPTool 95 | func processData(input: String) async throws -> String { 96 | // ... async throwing implementation 97 | } 98 | ``` 99 | 100 | ## Return Types 101 | 102 | The return type of an MCP tool must conform to both `Sendable` and `Codable`. This includes: 103 | 104 | - All basic types (`Int`, `Double`, `Float`, `String`, `Bool`) 105 | - Arrays of basic types 106 | - Custom types that conform to both protocols 107 | - `Void` (for functions that don't return a value) 108 | 109 | ## Default Values 110 | 111 | Parameters can have default values: 112 | 113 | ```swift 114 | @MCPTool 115 | func greet(name: String = "World", times: Int = 1) -> String { 116 | return String(repeating: "Hello, \(name)! ", count: times) 117 | } 118 | ``` 119 | 120 | ## Tips 121 | 122 | - For enum parameters, ensure they conform to `CaseIterable` and `Sendable` 123 | - By default, case labels are used for serialization 124 | - You can customize enum string representation by implementing `CustomStringConvertible` 125 | - Raw values are ignored for serialization purposes 126 | - For custom types, ensure they conform to both `Sendable` and `Codable` 127 | - Default values are supported for all parameter types 128 | - Function parameters and return types must be `Sendable` to ensure thread safety 129 | 130 | ## Topics 131 | 132 | ### Related Articles 133 | - ``MCPServer`` 134 | - ``MCPTool`` -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Resources/01-calculator-base.swift: -------------------------------------------------------------------------------- 1 | /** 2 | A Calculator for simple math operations like addition, subtraction, and more. 3 | */ 4 | class Calculator { 5 | /** 6 | Adds two numbers together and returns their sum. 7 | 8 | - Parameter a: First number to add 9 | - Parameter b: Second number to add 10 | - Returns: The sum of a and b 11 | */ 12 | func add(a: Double, b: Double) -> Double { 13 | return a + b 14 | } 15 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Resources/02-calculator-server.swift: -------------------------------------------------------------------------------- 1 | import SwiftMCP 2 | 3 | /** 4 | A Calculator for simple math operations like addition, subtraction, and more. 5 | */ 6 | @MCPServer(version: "1.0.0") 7 | class Calculator { 8 | /** 9 | Adds two numbers together and returns their sum. 10 | 11 | - Parameter a: First number to add 12 | - Parameter b: Second number to add 13 | - Returns: The sum of a and b 14 | */ 15 | func add(a: Double, b: Double) -> Double { 16 | return a + b 17 | } 18 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Resources/03-calculator-tool.swift: -------------------------------------------------------------------------------- 1 | import SwiftMCP 2 | 3 | /** 4 | A Calculator for simple math operations like addition, subtraction, and more. 5 | */ 6 | @MCPServer(version: "1.0.0") 7 | class Calculator { 8 | /** 9 | Adds two numbers together and returns their sum. 10 | 11 | - Parameter a: First number to add 12 | - Parameter b: Second number to add 13 | - Returns: The sum of a and b 14 | */ 15 | @MCPTool 16 | func add(a: Double, b: Double) -> Double { 17 | return a + b 18 | } 19 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Resources/04-calculator-named.swift: -------------------------------------------------------------------------------- 1 | import SwiftMCP 2 | 3 | /** 4 | A Calculator for simple math operations like addition, subtraction, and more. 5 | */ 6 | @MCPServer(version: "1.0.0", name: "SwiftMCP Demo") 7 | class Calculator { 8 | /** 9 | Adds two numbers together and returns their sum. 10 | 11 | - Parameter a: First number to add 12 | - Parameter b: Second number to add 13 | - Returns: The sum of a and b 14 | */ 15 | @MCPTool 16 | func add(a: Double, b: Double) -> Double { 17 | return a + b 18 | } 19 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Resources/04-greeting-error.swift: -------------------------------------------------------------------------------- 1 | /* 2 | A custom error type for handling validation errors in the MCP server. 3 | */ 4 | import Foundation 5 | 6 | enum GreetingError: LocalizedError { 7 | case nameTooShort 8 | 9 | var errorDescription: String? { 10 | switch self { 11 | case .nameTooShort: 12 | return "Name must be at least 2 characters long" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Resources/05-calculator-description.swift: -------------------------------------------------------------------------------- 1 | import SwiftMCP 2 | 3 | /** 4 | A Calculator for simple math operations like addition, subtraction, and more. 5 | */ 6 | @MCPServer(version: "1.0.0", name: "SwiftMCP Demo") 7 | class Calculator { 8 | /** 9 | Adds two numbers together and returns their sum. 10 | 11 | - Parameter a: First number to add 12 | - Parameter b: Second number to add 13 | - Returns: The sum of a and b 14 | */ 15 | @MCPTool(description: "Performs addition of two numbers") 16 | func add(a: Double, b: Double) -> Double { 17 | return a + b 18 | } 19 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Resources/05-calculator-throwing.swift: -------------------------------------------------------------------------------- 1 | import SwiftMCP 2 | 3 | @MCPServer(version: "1.0.0") 4 | struct Calculator { 5 | @MCPTool(description: "Adds two numbers together") 6 | func add(a: Double, b: Double) -> Double { 7 | return a + b 8 | } 9 | 10 | @MCPTool(description: "Greets a person by name") 11 | func greet(name: String) throws -> String { 12 | guard name.count >= 2 else { 13 | throw GreetingError.nameTooShort 14 | } 15 | return "Hello, \(name)!" 16 | } 17 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Resources/06-calculator-async.swift: -------------------------------------------------------------------------------- 1 | import SwiftMCP 2 | 3 | @MCPServer(version: "1.0.0") 4 | struct Calculator { 5 | @MCPTool(description: "Adds two numbers together") 6 | func add(a: Double, b: Double) -> Double { 7 | return a + b 8 | } 9 | 10 | @MCPTool(description: "Greets a person by name") 11 | func greet(name: String) throws -> String { 12 | guard name.count >= 2 else { 13 | throw GreetingError.nameTooShort 14 | } 15 | return "Hello, \(name)!" 16 | } 17 | 18 | @MCPTool(description: "Sends a delayed greeting") 19 | func delayedGreet(name: String) async throws -> String { 20 | try await Task.sleep(for: .seconds(1)) 21 | return try greet(name: name) 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Resources/06-calculator-schema.swift: -------------------------------------------------------------------------------- 1 | import SwiftMCP 2 | 3 | /** 4 | A Calculator for simple math operations like addition, subtraction, and more. 5 | */ 6 | @MCPServer(version: "1.0.0", name: "SwiftMCP Demo") 7 | class Calculator { 8 | /** 9 | Adds two numbers together and returns their sum. 10 | 11 | This documentation comment will be used to generate the OpenAPI schema 12 | for this tool, including the parameter descriptions and return value. 13 | 14 | - Parameter a: First number to add 15 | - Parameter b: Second number to add 16 | - Returns: The sum of a and b 17 | */ 18 | @MCPTool(description: "Sends a delayed greeting") 19 | func add(a: Double, b: Double) -> Double { 20 | return a + b 21 | } 22 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Resources/placeholder.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/SwiftMCP.md: -------------------------------------------------------------------------------- 1 | # ``SwiftMCP`` 2 | 3 | A Swift framework that makes it easy to expose your functions as Model Context Protocol (MCP) tools for AI assistants. 4 | 5 | ## Overview 6 | 7 | SwiftMCP lets you turn any Swift function into an MCP tool with just a single decorator. It handles all the complexity of JSON-RPC communication, parameter validation, and documentation generation, letting you focus on writing your tool's logic. 8 | 9 | ```swift 10 | @MCPServer 11 | class Calculator { 12 | @MCPTool 13 | func add(a: Int, b: Int) -> Int { 14 | a + b 15 | } 16 | } 17 | ``` 18 | 19 | The framework automatically: 20 | - Extracts documentation from your Swift comments to describe tools 21 | - Validates and converts parameters to the correct types 22 | - Generates OpenAPI specifications for AI integration 23 | - Provides multiple transport options (HTTP+SSE, stdio) 24 | 25 | ### Key Features 26 | 27 | - **Documentation-Driven**: Your standard Swift documentation comments are automatically turned into tool descriptions, parameter info, and return type documentation. 28 | - **Type-Safe**: All parameters are automatically validated and converted to their correct Swift types. 29 | - **AI-Ready**: Built-in support for OpenAPI specification generation and AI plugin manifests. 30 | - **Flexible Transport**: Choose between HTTP+SSE for web applications or stdio for command-line tools. 31 | 32 | ## Next Steps 33 | 34 | Start with to create your first MCP server, then explore to understand how SwiftMCP uses documentation to power AI interactions. 35 | 36 | ## Topics 37 | 38 | ### Getting Started 39 | 40 | - 41 | - 42 | - 43 | - 44 | 45 | ### Macros 46 | 47 | - ``MCPServer`` 48 | - ``MCPTool`` 49 | 50 | ### Core Types 51 | 52 | - ``MCPToolMetadata`` 53 | - ``MCPToolParameterInfo`` 54 | - ``MCPToolError`` 55 | 56 | ### Server Components 57 | 58 | - ``Transport`` 59 | - ``HTTPSSETransport`` 60 | - ``StdioTransport`` 61 | 62 | ```swift 63 | @MCPServer(version: "1.0.0") 64 | struct Calculator { 65 | @MCPTool(description: "Adds two numbers") 66 | func add(a: Double, b: Double) -> Double { 67 | return a + b 68 | } 69 | } 70 | ``` -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Tutorials/BuildingAnMCPServer.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorial(time: 30) { 2 | @Intro(title: "Building an MCP Server") { 3 | Learn how to build a full-featured MCP server that can interact with AI models. 4 | 5 | You'll learn how to create a simple calculator server and add documentation that integrates with the OpenAPI schema generation. 6 | 7 | @Image(source: "placeholder", alt: "Illustration showing the basic structure of an MCP server") 8 | } 9 | 10 | @Section(title: "Creating Your First MCP Server") { 11 | @ContentAndMedia { 12 | Learn how to transform a regular Swift class into an MCP server by adding documentation and macros. 13 | 14 | @Image(source: "placeholder", alt: "Illustration showing the transformation of a regular class into an MCP server") 15 | } 16 | 17 | @Steps { 18 | @Step { 19 | Start with a basic calculator class that has proper documentation. 20 | 21 | @Code(name: "Calculator.swift", file: "01-calculator-base.swift") 22 | } 23 | 24 | @Step { 25 | Import SwiftMCP and add the MCPServer macro with version information. 26 | 27 | @Code(name: "Calculator.swift", file: "02-calculator-server.swift") 28 | } 29 | 30 | @Step { 31 | Add the MCPTool macro to expose the add function. 32 | 33 | @Code(name: "Calculator.swift", file: "03-calculator-tool.swift") 34 | } 35 | 36 | @Step { 37 | Give your server a custom name using the MCPServer macro. 38 | 39 | @Code(name: "Calculator.swift", file: "04-calculator-named.swift") 40 | } 41 | 42 | @Step { 43 | Add a custom description to the MCPTool macro. 44 | 45 | @Code(name: "Calculator.swift", file: "05-calculator-description.swift") 46 | } 47 | 48 | @Step { 49 | Notice how the documentation comments are used to generate the OpenAPI schema. 50 | 51 | @Code(name: "Calculator.swift", file: "06-calculator-schema.swift") 52 | } 53 | } 54 | } 55 | 56 | @Section(title: "Adding Error Handling") { 57 | @ContentAndMedia { 58 | Learn how to handle errors and validate input in your MCP server. 59 | 60 | @Image(source: "placeholder", alt: "Illustration showing error handling in an MCP server") 61 | } 62 | 63 | @Steps { 64 | @Step { 65 | Create a custom error type for input validation. 66 | 67 | @Code(name: "GreetingError.swift", file: "04-greeting-error.swift") 68 | } 69 | 70 | @Step { 71 | Add a throwing function that validates input. 72 | 73 | @Code(name: "Calculator.swift", file: "05-calculator-throwing.swift") 74 | } 75 | } 76 | } 77 | 78 | @Section(title: "Adding Async Support") { 79 | @ContentAndMedia { 80 | Learn how to add asynchronous operations to your MCP server. 81 | 82 | @Image(source: "placeholder", alt: "Illustration showing async operations in an MCP server") 83 | } 84 | 85 | @Steps { 86 | @Step { 87 | Add an async function that simulates a network delay. 88 | 89 | @Code(name: "Calculator.swift", file: "06-calculator-async.swift") 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.docc/Tutorials/SwiftMCPTutorials.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorials(name: "SwiftMCP Tutorials") { 2 | @Intro(title: "Building MCP Servers") { 3 | Learn how to build powerful Model Context Protocol (MCP) servers using SwiftMCP. 4 | 5 | These tutorials will guide you through creating MCP servers that can interact with AI models, 6 | handle errors gracefully, and perform asynchronous operations. 7 | 8 | @Image(source: "placeholder.png", alt: "Illustration showing the SwiftMCP framework architecture") 9 | } 10 | 11 | @Chapter(name: "Getting Started with SwiftMCP") { 12 | Learn the fundamentals of building MCP servers with SwiftMCP. 13 | 14 | @Image(source: "placeholder.png", alt: "Illustration showing a basic MCP server structure") 15 | 16 | @TutorialReference(tutorial: "doc:BuildingAnMCPServer") 17 | } 18 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/SwiftMCP.swift: -------------------------------------------------------------------------------- 1 | /// SwiftMCP is a framework for building Model-Controller-Protocol (MCP) servers. 2 | /// 3 | /// The framework provides: 4 | /// - Easy-to-use macros for exposing Swift functions to AI models 5 | /// - Automatic JSON-RPC communication handling 6 | /// - Type-safe parameter validation and conversion 7 | /// - Support for async/await and error handling 8 | /// - Multiple transport options (stdio and HTTP/SSE) 9 | /// - OpenAPI specification generation 10 | /// - Resource management capabilities 11 | /// 12 | /// To get started, see the ``MCPServer`` protocol and the ``MCPTool`` macro. 13 | public enum SwiftMCP { 14 | /// The current version of the SwiftMCP framework 15 | public static let version = "1.0.0" 16 | } -------------------------------------------------------------------------------- /Sources/SwiftMCP/Transport/Channel+SSE.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOCore 3 | import NIOHTTP1 4 | import Logging 5 | 6 | extension Channel { 7 | /// Send an SSE message through the channel 8 | /// - Parameter message: The SSE message to send 9 | /// - Returns: An EventLoopFuture that completes when the message has been written and flushed 10 | func sendSSE(_ message: LosslessStringConvertible) { 11 | guard self.isActive else { 12 | 13 | return 14 | } 15 | 16 | let messageText = message.description 17 | var buffer = self.allocator.buffer(capacity: messageText.utf8.count) 18 | buffer.writeString(message.description) 19 | 20 | let part = HTTPServerResponsePart.body(.byteBuffer(buffer)) 21 | 22 | // Write with promise 23 | self.write(part, promise: nil) 24 | self.flush() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Transport/RequestState.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | import NIOCore 3 | 4 | /// Represents the state of an HTTP request being processed 5 | enum RequestState { 6 | case idle 7 | case head(HTTPRequestHead) 8 | case body(head: HTTPRequestHead, data: ByteBuffer) 9 | } 10 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Transport/SSEChannelManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSEChannelManager.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 18.03.25. 6 | // 7 | 8 | import Foundation 9 | import NIO 10 | 11 | actor SSEChannelManager { 12 | private var sseChannels: [UUID: Channel] = [:] 13 | 14 | /// Returns the current number of active SSE channels. 15 | var channelCount: Int { 16 | sseChannels.count 17 | } 18 | 19 | /// Register a new SSE channel. 20 | /// - Parameters: 21 | /// - channel: The channel to register. 22 | /// - id: The unique identifier for the channel. 23 | func register(channel: Channel, id: UUID) { 24 | // Only register if the channel isn't already present. 25 | guard sseChannels[id] == nil else { return } 26 | sseChannels[id] = channel 27 | } 28 | 29 | /// Remove an SSE channel. 30 | /// - Parameter id: The unique identifier of the channel. 31 | /// - Returns: True if a channel was removed. 32 | @discardableResult 33 | func removeChannel(id: UUID) -> Bool { 34 | if sseChannels.removeValue(forKey: id) != nil { 35 | return true 36 | } 37 | return false 38 | } 39 | 40 | /// Broadcast an SSE message to all channels. 41 | /// - Parameter message: The SSE message to send. 42 | func broadcastSSE(_ message: SSEMessage) { 43 | for channel in sseChannels.values { 44 | channel.sendSSE(message) 45 | } 46 | } 47 | 48 | /// Send an SSE message to a channel identified by a clientId string. 49 | /// - Parameters: 50 | /// - message: The SSE message to send. 51 | /// - clientIdString: The string that can be converted to a UUID. 52 | func sendSSE(_ message: SSEMessage, to clientIdString: String) { 53 | guard let uuid = UUID(uuidString: clientIdString), 54 | let channel = sseChannels[uuid], 55 | channel.isActive else { return } 56 | channel.sendSSE(message) 57 | } 58 | 59 | /// Retrieve the channel for a given client identifier. 60 | /// - Parameter clientIdString: The string representation of the UUID. 61 | /// - Returns: The channel if found. 62 | func getChannel(for clientIdString: String) -> Channel? { 63 | guard let uuid = UUID(uuidString: clientIdString) else { return nil } 64 | return sseChannels[uuid] 65 | } 66 | 67 | /// Close all channels and remove them. 68 | func stopAllChannels() { 69 | for channel in sseChannels.values { 70 | channel.close(promise: nil) 71 | } 72 | sseChannels.removeAll() 73 | } 74 | 75 | /// Check if there's an active SSE connection for a given client. 76 | /// - Parameter clientIdString: The string representation of the UUID. 77 | /// - Returns: True if there's an active channel for this client. 78 | func hasActiveConnection(for clientIdString: String) -> Bool { 79 | guard let uuid = UUID(uuidString: clientIdString), 80 | let channel = sseChannels[uuid] else { return false } 81 | return channel.isActive 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Transport/SSEMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSEMessage.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 17.03.25. 6 | // 7 | 8 | 9 | import Foundation 10 | 11 | /// A Server-Sent Events (SSE) message 12 | struct SSEMessage: LosslessStringConvertible { 13 | let name: String? 14 | let data: String 15 | 16 | init(name: String? = nil, data: String) { 17 | self.name = name 18 | self.data = data 19 | } 20 | 21 | /// Creates an SSE message from a string representation 22 | /// Expects format: 23 | /// [event: name\n] 24 | /// data: content\n\n 25 | init?(_ description: String) { 26 | // Split the message into lines 27 | let lines = description.split(separator: "\n", omittingEmptySubsequences: false) 28 | var name: String? = nil 29 | var data: String? = nil 30 | 31 | for line in lines { 32 | if line.starts(with: "event:") { 33 | name = String(line.dropFirst(6)).trimmingCharacters(in: .whitespaces) 34 | } else if line.starts(with: "data:") { 35 | data = String(line.dropFirst(5)).trimmingCharacters(in: .whitespaces) 36 | } 37 | } 38 | 39 | // Data field is required 40 | guard let data = data else { 41 | return nil 42 | } 43 | 44 | self.name = name 45 | self.data = data 46 | } 47 | 48 | /// Returns the string representation of the message in SSE format 49 | var description: String { 50 | var message = "" 51 | if let name = name { 52 | message += "event: \(name)\n" 53 | } 54 | message += "data: \(data)\n\n" 55 | return message 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Transport/Transport.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Logging 3 | 4 | /** 5 | Protocol defining the common interface for MCP (Model Context Protocol) transports. 6 | 7 | Transport implementations handle the communication layer between clients and the MCP server. 8 | Each transport type provides a different way to interact with the server: 9 | 10 | - HTTP+SSE: Provides HTTP endpoints with Server-Sent Events for real-time updates 11 | - Stdio: Uses standard input/output for command-line integration 12 | 13 | - Important: All transport implementations must be thread-safe and handle concurrent requests appropriately. 14 | 15 | - Note: Transport implementations should properly clean up resources in their `stop()` method. 16 | 17 | ## Example Usage 18 | ```swift 19 | class MyTransport: Transport { 20 | let server: MCPServer 21 | let logger = Logger(label: "com.example.MyTransport") 22 | 23 | init(server: MCPServer) { 24 | self.server = server 25 | } 26 | 27 | func start() async throws { 28 | // Initialize and start your transport 29 | } 30 | 31 | func run() async throws { 32 | try await start() 33 | // Block until stopped 34 | } 35 | 36 | func stop() async throws { 37 | // Clean up resources 38 | } 39 | } 40 | ``` 41 | */ 42 | public protocol Transport { 43 | /** 44 | The MCP server instance being exposed by this transport. 45 | 46 | This server handles the actual processing of requests and maintains the available tools/functions. 47 | The transport is responsible for getting requests to and responses from this server instance. 48 | */ 49 | var server: MCPServer { get } 50 | 51 | /** 52 | Logger instance for this transport. 53 | 54 | Used to log transport-specific events, errors, and debug information. 55 | Each transport implementation should use a unique label for its logger. 56 | */ 57 | var logger: Logger { get } 58 | 59 | /** 60 | Initialize a new transport with an MCP server. 61 | 62 | - Parameter server: The MCP server to expose through this transport 63 | */ 64 | init(server: MCPServer) 65 | 66 | /** 67 | Start the transport in a non-blocking way. 68 | 69 | This method should initialize the transport and make it ready to handle requests, 70 | but should return immediately without blocking the calling thread. 71 | 72 | - Throws: Any errors that occur during startup, such as: 73 | - Port binding failures 74 | - Configuration errors 75 | - Resource allocation failures 76 | 77 | - Important: This method should be idempotent. Calling it multiple times should not create 78 | multiple instances of the transport. 79 | */ 80 | func start() async throws 81 | 82 | /** 83 | Run the transport and block until stopped. 84 | 85 | This method should start the transport if it hasn't been started yet and then 86 | block until the transport is explicitly stopped or encounters a fatal error. 87 | 88 | - Throws: Any errors that occur during operation, such as: 89 | - Startup errors if the transport hasn't been started 90 | - Fatal runtime errors 91 | - Shutdown errors 92 | 93 | - Note: This is typically used for command-line tools or services that should 94 | run until explicitly terminated. 95 | */ 96 | func run() async throws 97 | 98 | /** 99 | Stop the transport gracefully. 100 | 101 | This method should: 102 | 1. Stop accepting new connections/requests 103 | 2. Complete any in-flight requests if possible 104 | 3. Release all resources 105 | 4. Shut down cleanly 106 | 107 | - Throws: Any errors that occur during shutdown, such as: 108 | - Resource cleanup failures 109 | - Timeout errors 110 | - IO errors 111 | 112 | - Important: This method should be idempotent. Calling it multiple times should 113 | not cause errors. 114 | */ 115 | func stop() async throws 116 | } 117 | -------------------------------------------------------------------------------- /Sources/SwiftMCP/Transport/TransportError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransportError.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 21.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Errors that can occur during transport operations in SwiftMCP. 12 | 13 | This enum provides specific error types and localized descriptions for various 14 | transport-related failures, such as binding failures when starting a server. 15 | */ 16 | public enum TransportError: LocalizedError { 17 | /** 18 | Indicates that the transport failed to bind to a specific address and port. 19 | 20 | - Parameter message: A human-readable description of the binding failure, 21 | including details about the specific cause (e.g., port in use, permission denied). 22 | */ 23 | case bindingFailed(String) 24 | 25 | /** 26 | Provides a localized description of the error. 27 | 28 | - Returns: A human-readable string describing the error. 29 | */ 30 | public var errorDescription: String? { 31 | switch self { 32 | case .bindingFailed(let message): 33 | return message 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftMCPMacros/MCPDiagnostics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MCPDiagnostics.swift 3 | // SwiftMCPMacros 4 | // 5 | // Created by Oliver Drobnik on 08.03.25. 6 | // 7 | 8 | import Foundation 9 | import SwiftDiagnostics 10 | import SwiftSyntax 11 | import SwiftSyntaxBuilder 12 | import SwiftSyntaxMacros 13 | 14 | /** 15 | Diagnostic messages for the MCP macros. 16 | 17 | This enum defines the diagnostic messages that can be emitted by the MCP macros, 18 | including errors and warnings related to function declarations. 19 | */ 20 | enum MCPToolDiagnostic: DiagnosticMessage { 21 | /// Error when the macro is applied to a non-function declaration 22 | case onlyFunctions 23 | 24 | /// Warning when a function is missing a description 25 | case missingDescription(functionName: String) 26 | 27 | /// Error when a parameter has an unsupported default value type 28 | case invalidDefaultValueType(paramName: String, typeName: String) 29 | 30 | /// Error when a parameter has an unsupported closure type 31 | case closureTypeNotSupported(paramName: String, typeName: String) 32 | 33 | /// Error when an optional parameter is missing a default value 34 | case optionalParameterNeedsDefault(paramName: String, typeName: String) 35 | 36 | var message: String { 37 | switch self { 38 | case .onlyFunctions: 39 | return "The MCPTool macro can only be applied to functions" 40 | case .missingDescription(let functionName): 41 | return "Function '\(functionName)' is missing a description. Add a documentation comment or provide a description parameter." 42 | case .invalidDefaultValueType(let paramName, let typeName): 43 | return "Parameter '\(paramName)' has an unsupported default value type '\(typeName)'. Only numbers, booleans, and strings are supported." 44 | case .closureTypeNotSupported(let paramName, let typeName): 45 | return "Parameter '\(paramName)' has an unsupported closure type '\(typeName)'. Closures are not supported in MCP tools." 46 | case .optionalParameterNeedsDefault(let paramName, let typeName): 47 | return "Optional parameter '\(paramName)' of type '\(typeName)' requires a default value (e.g. = nil)." 48 | } 49 | } 50 | 51 | var severity: DiagnosticSeverity { 52 | switch self { 53 | case .onlyFunctions, .invalidDefaultValueType, .closureTypeNotSupported, .optionalParameterNeedsDefault: 54 | return .error 55 | case .missingDescription: 56 | return .warning 57 | } 58 | } 59 | 60 | var diagnosticID: MessageID { 61 | switch self { 62 | case .onlyFunctions: 63 | return MessageID(domain: "SwiftMCP", id: "onlyFunctions") 64 | case .missingDescription: 65 | return MessageID(domain: "SwiftMCP", id: "missingDescription") 66 | case .invalidDefaultValueType: 67 | return MessageID(domain: "SwiftMCP", id: "invalidDefaultValueType") 68 | case .closureTypeNotSupported: 69 | return MessageID(domain: "SwiftMCP", id: "closureTypeNotSupported") 70 | case .optionalParameterNeedsDefault: 71 | return MessageID(domain: "SwiftMCP", id: "optionalParameterNeedsDefault") 72 | } 73 | } 74 | } 75 | 76 | enum MCPToolFixItMessage: FixItMessage { 77 | case addDefaultValue(paramName: String) 78 | 79 | var message: String { 80 | switch self { 81 | case .addDefaultValue(let paramName): 82 | return "Add default value '= nil' for parameter '\(paramName)'" 83 | } 84 | } 85 | 86 | var diagnosticID: MessageID { 87 | switch self { 88 | case .addDefaultValue: 89 | return MessageID(domain: "SwiftMCP", id: "addDefaultValue") 90 | } 91 | } 92 | 93 | var fixItID: MessageID { 94 | switch self { 95 | case .addDefaultValue: 96 | return MessageID(domain: "SwiftMCP", id: "addDefaultValue") 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/SwiftMCPMacros/MCPPromptMacro.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftDiagnostics 3 | import SwiftSyntax 4 | import SwiftSyntaxBuilder 5 | import SwiftSyntaxMacros 6 | 7 | public struct MCPPromptMacro: PeerMacro { 8 | public static func expansion( 9 | of node: AttributeSyntax, 10 | providingPeersOf declaration: some DeclSyntaxProtocol, 11 | in context: some MacroExpansionContext 12 | ) throws -> [DeclSyntax] { 13 | guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else { 14 | let diagnostic = Diagnostic(node: Syntax(node), message: MCPToolDiagnostic.onlyFunctions) 15 | context.diagnose(diagnostic) 16 | return [] 17 | } 18 | 19 | let extractor = FunctionMetadataExtractor(funcDecl: funcDecl, context: context) 20 | let metadata = try extractor.extract() 21 | let functionName = metadata.functionName 22 | 23 | var descriptionArg = "nil" 24 | if let args = node.arguments?.as(LabeledExprListSyntax.self) { 25 | for arg in args { 26 | if arg.label?.text == "description", 27 | let stringLiteral = arg.expression.as(StringLiteralExprSyntax.self) { 28 | let stringValue = stringLiteral.segments.description 29 | descriptionArg = "\"\(stringValue.escapedForSwiftString)\"" 30 | } 31 | } 32 | } 33 | if descriptionArg == "nil" { 34 | if !metadata.documentation.description.isEmpty { 35 | descriptionArg = "\"\(metadata.documentation.description.escapedForSwiftString)\"" 36 | } 37 | } 38 | if descriptionArg == "nil" && functionName != "missingDescription" { 39 | let diagnostic = Diagnostic(node: Syntax(funcDecl.name), message: MCPToolDiagnostic.missingDescription(functionName: functionName)) 40 | context.diagnose(diagnostic) 41 | } 42 | 43 | var paramInfoStrings: [String] = [] 44 | for param in metadata.parameters { 45 | paramInfoStrings.append(param.toMCPParameterInfo()) 46 | } 47 | 48 | let metadataDeclaration = """ 49 | /// Metadata for the \(functionName) prompt 50 | nonisolated private let __mcpPromptMetadata_\(functionName) = MCPPromptMetadata( 51 | name: \"\(functionName)\", 52 | description: \(descriptionArg), 53 | parameters: [\(paramInfoStrings.joined(separator: ", "))], 54 | isAsync: \(metadata.isAsync), 55 | isThrowing: \(metadata.isThrowing) 56 | ) 57 | """ 58 | 59 | var wrapperFunc = """ 60 | /// Autogenerated wrapper for \(functionName) that takes a dictionary of parameters 61 | func __mcpPromptCall_\(functionName)(_ enrichedArguments: [String: Sendable]) async throws -> [PromptMessage] { 62 | """ 63 | 64 | for detail in metadata.parameters { 65 | wrapperFunc += """ 66 | let \(detail.name): \(detail.typeString) = try enrichedArguments.extractValue(named: \"\(detail.name)\", as: \(detail.typeString).self) 67 | """ 68 | } 69 | 70 | let parameterList = metadata.parameters.map { param in 71 | param.label == "_" ? param.name : "\(param.label): \(param.name)" 72 | }.joined(separator: ", ") 73 | 74 | wrapperFunc += """ 75 | let result = \(metadata.isThrowing ? "try " : "")\(metadata.isAsync ? "await " : "")\(functionName)(\(parameterList)) 76 | return PromptMessage.fromResult(result) 77 | } 78 | """ 79 | 80 | return [ 81 | DeclSyntax(stringLiteral: metadataDeclaration), 82 | DeclSyntax(stringLiteral: wrapperFunc) 83 | ] 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/SwiftMCPMacros/MCPResourceDiagnostics.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftDiagnostics 3 | import SwiftSyntax 4 | import SwiftSyntaxBuilder 5 | import SwiftSyntaxMacros 6 | 7 | /// Diagnostic messages for the `MCPResource` macro. 8 | enum MCPResourceDiagnostic: DiagnosticMessage { 9 | case onlyFunctions 10 | case requiresStringLiteral 11 | case missingParameterForPlaceholder(placeholder: String) // E001 12 | case unknownPlaceholder(parameterName: String) // E002 13 | case optionalParameterNeedsDefault(paramName: String) // E003 14 | case invalidURITemplate(reason: String) // E004 15 | 16 | var message: String { 17 | switch self { 18 | case .onlyFunctions: 19 | return "The MCPResource macro can only be applied to functions" 20 | case .requiresStringLiteral: 21 | return "URI template must be string or string array" 22 | case .missingParameterForPlaceholder(let ph): 23 | return "Missing parameter for placeholder '{\(ph)}'" 24 | case .unknownPlaceholder(let name): 25 | return "Unknown placeholder '{\(name)}' – not present in template" 26 | case .optionalParameterNeedsDefault(let name): 27 | return "Optional parameter '\(name)' requires a default value" 28 | case .invalidURITemplate(let reason): 29 | return "Invalid URI template: \(reason)" 30 | } 31 | } 32 | 33 | var severity: DiagnosticSeverity { .error } 34 | 35 | var diagnosticID: MessageID { 36 | switch self { 37 | case .missingParameterForPlaceholder: 38 | return MessageID(domain: "SwiftMCP", id: "E001") 39 | case .unknownPlaceholder: 40 | return MessageID(domain: "SwiftMCP", id: "E002") 41 | case .optionalParameterNeedsDefault: 42 | return MessageID(domain: "SwiftMCP", id: "E003") 43 | case .invalidURITemplate: 44 | return MessageID(domain: "SwiftMCP", id: "E004") 45 | case .onlyFunctions: 46 | return MessageID(domain: "SwiftMCP", id: "OnlyFunctions") 47 | case .requiresStringLiteral: 48 | return MessageID(domain: "SwiftMCP", id: "RequiresStringLiteral") 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SwiftMCPMacros/MCPServerDiagnostics.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftDiagnostics 3 | import SwiftSyntax 4 | import SwiftSyntaxBuilder 5 | import SwiftSyntaxMacros 6 | 7 | /** 8 | Diagnostic messages for the MCPServer macro. 9 | 10 | This enum defines the diagnostic messages that can be emitted by the MCPServer macro, 11 | including errors and warnings related to server declarations. 12 | */ 13 | enum MCPServerDiagnostic: DiagnosticMessage { 14 | /// Error when the macro is applied to a non-class declaration 15 | case requiresReferenceType(typeName: String) 16 | 17 | var message: String { 18 | switch self { 19 | case .requiresReferenceType(let typeName): 20 | return "'\(typeName)' must be a reference type (class or actor)" 21 | } 22 | } 23 | 24 | var severity: DiagnosticSeverity { 25 | switch self { 26 | case .requiresReferenceType: 27 | return .error 28 | } 29 | } 30 | 31 | var diagnosticID: MessageID { 32 | switch self { 33 | case .requiresReferenceType: 34 | return MessageID(domain: "SwiftMCPMacros", id: "RequiresReferenceType") 35 | } 36 | } 37 | } 38 | 39 | /// Fix-it messages for the MCPServer macro 40 | enum MCPServerFixItMessage: FixItMessage { 41 | case replaceWithClass(keyword: String) 42 | 43 | var message: String { 44 | switch self { 45 | case .replaceWithClass(let keyword): 46 | return "Change '\(keyword)' to 'class'" 47 | } 48 | } 49 | 50 | var fixItID: MessageID { 51 | switch self { 52 | case .replaceWithClass: 53 | return MessageID(domain: "SwiftMCPMacros", id: "ReplaceWithClass") 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/SwiftMCPMacros/String+Documentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Documentation.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 27.03.25. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | var removingUnprintableCharacters: String { 12 | // Create a character set of printable ASCII characters (32-126) plus newline, tab, etc. 13 | let printableCharacters = CharacterSet(charactersIn: " \t\n\r").union(CharacterSet(charactersIn: UnicodeScalar(32)...UnicodeScalar(126))) 14 | 15 | // Filter out any characters that are not in the printable set 16 | return unicodeScalars.filter { printableCharacters.contains($0) }.map { String($0) }.joined() 17 | } 18 | 19 | /// Escapes a string for use in a Swift string literal. 20 | /// This handles quotes, backslashes, and other special characters. 21 | var escapedForSwiftString: String { 22 | return self 23 | .replacingOccurrences(of: "\\", with: "\\\\") // Escape backslashes first 24 | .replacingOccurrences(of: "\"", with: "\\\"") // Escape double quotes 25 | .replacingOccurrences(of: "\'", with: "\\\'") // Escape single quotes 26 | .replacingOccurrences(of: "\n", with: "\\n") // Escape newlines 27 | .replacingOccurrences(of: "\r", with: "\\r") // Escape carriage returns 28 | .replacingOccurrences(of: "\t", with: "\\t") // Escape tabs 29 | .replacingOccurrences(of: "\0", with: "\\0") // Escape null bytes 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SwiftMCPMacros/SwiftMCPPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftMCPPlugin.swift 3 | // SwiftMCPMacros 4 | // 5 | // Created by Oliver Drobnik on 08.03.25. 6 | // 7 | 8 | import SwiftCompilerPlugin 9 | import SwiftSyntaxMacros 10 | 11 | /** 12 | Entry point for the Swift MCP compiler plugin. 13 | 14 | This struct conforms to the CompilerPlugin protocol and provides 15 | the macros available in this package. 16 | */ 17 | @main 18 | public struct SwiftMCPPlugin: CompilerPlugin { 19 | /// Public initializer required by the CompilerPlugin protocol 20 | public init() {} 21 | 22 | /// The macros provided by this plugin 23 | public var providingMacros: [Macro.Type] = [ 24 | MCPToolMacro.self, 25 | MCPServerMacro.self, 26 | SchemaMacro.self, 27 | MCPResourceMacro.self, 28 | MCPPromptMacro.self 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/Calculator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftMCP 3 | 4 | /** 5 | A Calculator for simple math doing additionals, subtractions etc. 6 | */ 7 | @MCPServer 8 | class Calculator { 9 | /// Adds two integers and returns their sum 10 | /// - Parameter a: First number to add 11 | /// - Parameter b: Second number to add 12 | /// - Returns: The sum of a and b 13 | @MCPTool(description: "Custom description: Performs addition of two numbers") 14 | func add(a: Int, b: Int) -> Int { 15 | return a + b 16 | } 17 | 18 | /// Subtracts the second integer from the first and returns the difference 19 | /// - Parameter a: Number to subtract from 20 | /// - Parameter b: Number to subtract 21 | /// - Returns: The difference between a and b 22 | @MCPTool 23 | func subtract(a: Int, b: Int = 3) -> Int { 24 | return a - b 25 | } 26 | 27 | /** 28 | Tests array processing 29 | - Parameter a: Array of integers to process 30 | - Returns: A string representation of the array 31 | */ 32 | @MCPTool(description: "Custom description: Tests array processing") 33 | func testArray(a: [Int]) -> String { 34 | return a.map(String.init).joined(separator: ", ") 35 | } 36 | 37 | /** 38 | Multiplies two integers and returns their product 39 | - Parameter a: First factor 40 | - Parameter b: Second factor 41 | - Returns: The product of a and b 42 | */ 43 | @MCPTool 44 | func multiply(a: Int, b: Int) -> Int { 45 | return a * b 46 | } 47 | 48 | /// Divides the numerator by the denominator and returns the quotient 49 | /// - Parameter numerator: Number to be divided 50 | /// - Parameter denominator: Number to divide by (defaults to 1.0) 51 | /// - Returns: The quotient of numerator divided by denominator 52 | @MCPTool 53 | func divide(numerator: Double, denominator: Double = 1.0) -> Double { 54 | return numerator / denominator 55 | } 56 | 57 | /// Returns a greeting message with the provided name 58 | /// - Parameter name: Name of the person to greet 59 | /// - Returns: The greeting message 60 | @MCPTool(description: "Shows a greeting message") 61 | func greet(name: String) async throws -> String { 62 | // Validate name length 63 | if name.count < 2 { 64 | throw DemoError.nameTooShort(name: name) 65 | } 66 | 67 | // Validate name contains only letters and spaces 68 | if !name.allSatisfy({ $0.isLetter || $0.isWhitespace }) { 69 | throw DemoError.invalidName(name: name) 70 | } 71 | 72 | return "Hello, \(name)!" 73 | } 74 | 75 | 76 | /** A simple ping function that returns 'pong' */ 77 | @MCPTool 78 | func ping() -> String { 79 | return "pong" 80 | } 81 | 82 | /** A function to test doing nothing, not returning anything*/ 83 | @MCPTool 84 | func noop() { 85 | 86 | } 87 | 88 | /** 89 | Gets the current date/time on the server 90 | - Returns: The current time 91 | */ 92 | @MCPTool 93 | func getCurrentDateTime() -> Date { 94 | return Date() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/CalculatorTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | import SwiftMCP 3 | 4 | @Test("Addition") 5 | func testAdd() async throws { 6 | let calculator = Calculator() 7 | 8 | // Test direct function call 9 | #expect(calculator.add(a: 2, b: 3) == 5) 10 | 11 | // Test through callTool 12 | let result = try await calculator.callTool("add", arguments: ["a": 2, "b": 3]) 13 | #expect(result as? Int == 5) 14 | } 15 | 16 | @Test 17 | func testTestArray() async throws { 18 | let calculator = Calculator() 19 | 20 | // Test direct function call 21 | #expect(calculator.testArray(a: [1, 2, 3]) == "1, 2, 3") 22 | 23 | // Test through callTool 24 | let result = try await calculator.callTool("testArray", arguments: ["a": [1, 2, 3]]) 25 | #expect(result as? String == "1, 2, 3") 26 | } 27 | 28 | @Test 29 | func testUnknownTool() async throws { 30 | let calculator = Calculator() 31 | 32 | do { 33 | _ = try await calculator.callTool("unknown", arguments: [:]) 34 | #expect(Bool(false), "Should throw an error for unknown tool") 35 | } catch let error as MCPToolError { 36 | if case .unknownTool(let name) = error { 37 | #expect(name == "unknown") 38 | } else { 39 | #expect(Bool(false), "Error should be unknownTool") 40 | } 41 | } catch { 42 | #expect(Bool(false), "Error should be MCPToolError") 43 | } 44 | } 45 | 46 | @Test 47 | func testInvalidArgumentType() async throws { 48 | let calculator = Calculator() 49 | 50 | do { 51 | _ = try await calculator.callTool("add", arguments: ["a": "not_a_number", "b": 3]) 52 | #expect(Bool(false), "Should throw an error for invalid argument type") 53 | } catch let error as MCPToolError { 54 | if case .invalidArgumentType(let parameterName, let expectedType, _) = error { 55 | #expect(parameterName == "a") 56 | #expect(expectedType == "Int") 57 | } else { 58 | #expect(Bool(false), "Error should be invalidArgumentType") 59 | } 60 | } catch { 61 | #expect(Bool(false), "Error should be MCPToolError") 62 | } 63 | } 64 | 65 | @Test("Bool values should be converted when Int parameters are expected") 66 | func testBoolToIntConversion() async throws { 67 | let calculator = Calculator() 68 | 69 | let result = try await calculator.callTool("add", arguments: [ 70 | "a": true, 71 | "b": false 72 | ]) 73 | 74 | #expect(result as? Int == 1) 75 | } 76 | 77 | @Test("Bool values should be converted when Double parameters are expected") 78 | func testBoolToDoubleConversion() async throws { 79 | let calculator = Calculator() 80 | 81 | let result = try await calculator.callTool("divide", arguments: [ 82 | "numerator": true, 83 | "denominator": true 84 | ]) 85 | 86 | #expect(result as? Double == 1.0) 87 | } 88 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/CompletionTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import SwiftMCP 4 | import AnyCodable 5 | 6 | @MCPServer 7 | class CompletionServer { 8 | enum Color: CaseIterable { 9 | case red 10 | case green 11 | case blue 12 | } 13 | 14 | @MCPResource("color://message?color={color}") 15 | func getColorMessage(color: Color) -> String { 16 | switch color { 17 | case .red: return "You selected RED!" 18 | case .green: return "You selected GREEN!" 19 | case .blue: return "You selected BLUE!" 20 | } 21 | } 22 | } 23 | 24 | @MCPServer 25 | class CustomCompletionServer: MCPCompletionProviding { 26 | enum Color: CaseIterable { case red, green, blue } 27 | 28 | @MCPResource("color://message?color={color}") 29 | func getColorMessage(color: Color) -> String { 30 | switch color { 31 | case .red: return "You selected RED!" 32 | case .green: return "You selected GREEN!" 33 | case .blue: return "You selected BLUE!" 34 | } 35 | } 36 | 37 | func completion(for parameter: MCPParameterInfo, in context: MCPCompletionContext, prefix: String) async -> CompleteResult.Completion { 38 | if parameter.name == "color" { 39 | let defaults = defaultEnumCompletion(for: parameter, prefix: prefix)?.values ?? [] 40 | let extra = ["ruby", "rose"].filter { $0.hasPrefix(prefix) }.sortedByBestCompletion(prefix: prefix) 41 | let all = extra + defaults 42 | return CompleteResult.Completion(values: all, total: all.count, hasMore: false) 43 | } 44 | return defaultEnumCompletion(for: parameter, prefix: prefix) ?? CompleteResult.Completion(values: []) 45 | } 46 | } 47 | 48 | @Test("Enum completion returns case labels with prefix match first") 49 | func testEnumCompletion() async throws { 50 | let server = CompletionServer() 51 | 52 | let request = JSONRPCMessage.request( 53 | id: 1, 54 | method: "completion/complete", 55 | params: [ 56 | "argument": ["name": "color", "value": "r"], 57 | "ref": ["type": "ref/resource", "uri": "color://message?color={color}"] 58 | ] 59 | ) 60 | 61 | guard let message = await server.handleMessage(request), 62 | case .response(let response) = message else { 63 | #expect(Bool(false), "Expected response") 64 | return 65 | } 66 | 67 | let result = unwrap(response.result) 68 | let comp = unwrap(result["completion"]?.value as? [String: Any]) 69 | let values = unwrap(comp["values"] as? [String]) 70 | 71 | #expect(values == ["red", "green", "blue"]) 72 | } 73 | 74 | @Test("Custom completion provider returns custom values") 75 | func testCustomCompletionProvider() async throws { 76 | let server = CustomCompletionServer() 77 | 78 | let request = JSONRPCMessage.request( 79 | id: 1, 80 | method: "completion/complete", 81 | params: [ 82 | "argument": ["name": "color", "value": "r"], 83 | "ref": ["type": "ref/resource", "uri": "color://message?color={color}"] 84 | ] 85 | ) 86 | 87 | guard let message = await server.handleMessage(request), 88 | case .response(let response) = message else { 89 | #expect(Bool(false), "Expected response") 90 | return 91 | } 92 | 93 | let result = unwrap(response.result) 94 | let comp = unwrap(result["completion"]?.value as? [String: Any]) 95 | let values = unwrap(comp["values"] as? [String]) 96 | 97 | #expect(values.first == "ruby") 98 | #expect(values.contains("red")) 99 | #expect(values.count == 5) 100 | } 101 | 102 | @Test("Completion sorting prefers longer prefix matches") 103 | func testSortedByBestCompletion() { 104 | let sorted = ["red", "green", "blue", "ruby"].sortedByBestCompletion(prefix: "re") 105 | #expect(sorted.first == "red") 106 | #expect(sorted[1] == "ruby") 107 | } 108 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/DemoError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Custom errors for the demo app 4 | public enum DemoError: LocalizedError { 5 | /// When a greeting name is too short 6 | case nameTooShort(name: String) 7 | 8 | /// When a greeting name contains invalid characters 9 | case invalidName(name: String) 10 | 11 | /// When the greeting service is temporarily unavailable 12 | case serviceUnavailable 13 | 14 | public var errorDescription: String? { 15 | switch self { 16 | case .nameTooShort(let name): 17 | return "Name '\(name)' is too short. Names must be at least 2 characters long." 18 | case .invalidName(let name): 19 | return "Name '\(name)' contains invalid characters. Only letters and spaces are allowed." 20 | case .serviceUnavailable: 21 | return "The greeting service is temporarily unavailable. Please try again later." 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/DictionaryEncoderTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | import AnyCodable 4 | @testable import SwiftMCP 5 | 6 | struct TestStruct: Codable, Equatable { 7 | let intValue: Int 8 | let stringValue: String 9 | let boolValue: Bool 10 | let doubleValue: Double 11 | } 12 | 13 | struct NestedStruct: Codable, Equatable { 14 | let name: String 15 | let inner: TestStruct 16 | } 17 | 18 | struct OptionalStruct: Codable, Equatable { 19 | let value: String? 20 | } 21 | 22 | @Test("DictionaryEncoder encodes flat struct to [String: AnyCodable]") 23 | func testFlatStruct() throws { 24 | let value = TestStruct(intValue: 42, stringValue: "hello", boolValue: true, doubleValue: 3.14) 25 | let encoder = DictionaryEncoder() 26 | let dict = try encoder.encode(value) 27 | #expect(dict["intValue"]?.value as? Int == 42) 28 | #expect(dict["stringValue"]?.value as? String == "hello") 29 | #expect(dict["boolValue"]?.value as? Bool == true) 30 | #expect(dict["doubleValue"]?.value as? Double == 3.14) 31 | } 32 | 33 | @Test("DictionaryEncoder encodes nested struct to [String: AnyCodable]") 34 | func testNestedStruct() throws { 35 | let value = NestedStruct(name: "outer", inner: TestStruct(intValue: 1, stringValue: "inner", boolValue: false, doubleValue: 2.71)) 36 | let encoder = DictionaryEncoder() 37 | let dict = try encoder.encode(value) 38 | #expect(dict["name"]?.value as? String == "outer") 39 | let inner = dict["inner"]?.value as? [String: Any] 40 | #expect(inner?["intValue"] as? Int == 1) 41 | #expect(inner?["stringValue"] as? String == "inner") 42 | #expect(inner?["boolValue"] as? Bool == false) 43 | #expect(inner?["doubleValue"] as? Double == 2.71) 44 | } 45 | 46 | @Test("DictionaryEncoder encodes arrays and optionals") 47 | func testArraysAndOptionals() throws { 48 | struct ArrayStruct: Codable { 49 | let items: [Int] 50 | let optional: String? 51 | } 52 | let value = ArrayStruct(items: [1,2,3], optional: nil) 53 | let encoder = DictionaryEncoder() 54 | let dict = try encoder.encode(value) 55 | #expect(dict["items"]?.value as? [Int] == [1,2,3]) 56 | #expect(dict["optional"] == nil) 57 | } 58 | 59 | @Test("DictionaryEncoder encodes non-nil optionals") 60 | func testNonNilOptionals() throws { 61 | struct OptionalStruct: Codable { 62 | let value: String? 63 | let number: Int? 64 | } 65 | let value = OptionalStruct(value: "present", number: nil) 66 | let encoder = DictionaryEncoder() 67 | let dict = try encoder.encode(value) 68 | #expect(dict["value"]?.value as? String == "present") 69 | #expect(dict["number"] == nil) // nil optional should be omitted 70 | } 71 | 72 | @Test("DictionaryEncoder encodes Date and Data") 73 | func testDateAndData() throws { 74 | struct SpecialStruct: Codable { 75 | let date: Date 76 | let data: Data 77 | } 78 | let now = Date(timeIntervalSince1970: 1234567890) 79 | let bytes = Data([0x01, 0x02, 0x03]) 80 | let value = SpecialStruct(date: now, data: bytes) 81 | let encoder = DictionaryEncoder() 82 | let dict = try encoder.encode(value) 83 | #expect(abs((dict["date"]?.value as? Double ?? 0) - 1234567890) < 0.001) 84 | #expect(dict["data"]?.value as? String == bytes.base64EncodedString()) 85 | } -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/Extensions/Array+CaseLabelsTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import SwiftMCP 3 | 4 | enum Options: CaseIterable { 5 | case all 6 | case unread 7 | case starred 8 | } 9 | 10 | @Suite("Array+CaseLabels") 11 | struct ArrayCaseLabelsTests { 12 | @Test("Case labels from enum") 13 | func testCaseLabelsFromEnum() throws { 14 | // Test that we get the correct labels for a CaseIterable enum 15 | let labels = Array(caseLabelsFrom: Options.self) 16 | #expect(labels != nil) 17 | #expect(labels == ["all", "unread", "starred"]) 18 | } 19 | 20 | @Test("Case labels from non-enum") 21 | func testCaseLabelsFromNonEnum() throws { 22 | // Test that we get nil for a non-CaseIterable type 23 | let labels = Array(caseLabelsFrom: Int.self) 24 | #expect(labels == nil) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/Extensions/StringContentTypeTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import SwiftMCP 3 | 4 | @Suite("String+ContentType") 5 | struct StringContentTypeTests { 6 | @Test("Exact match") 7 | func testExactMatch() throws { 8 | #expect("application/json".matchesAcceptHeader("application/json")) 9 | } 10 | 11 | @Test("Wildcard subtype") 12 | func testWildcardSubtype() throws { 13 | #expect("application/json".matchesAcceptHeader("application/*")) 14 | } 15 | 16 | @Test("Universal match") 17 | func testUniversalMatch() throws { 18 | #expect("application/json".matchesAcceptHeader("*/*")) 19 | } 20 | 21 | @Test("Non-match cases") 22 | func testNonMatch() throws { 23 | #expect(!"application/json".matchesAcceptHeader("text/html")) 24 | #expect(!"application/json".matchesAcceptHeader("text/*")) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/JSONRPCRequestTest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import SwiftMCP 4 | import AnyCodable 5 | 6 | @Test 7 | func testDecodeJSONRPCRequest() throws { 8 | let json = """ 9 | {"jsonrpc":"2.0","id":1,"method":"testMethod","params":{"foo":42}} 10 | """.data(using: .utf8)! 11 | let message = try JSONDecoder().decode(JSONRPCMessage.self, from: json) 12 | guard case .request(let request) = message else { 13 | throw TestError("Expected request case") 14 | } 15 | #expect(request.jsonrpc == "2.0") 16 | #expect(request.id == 1) 17 | #expect(request.method == "testMethod") 18 | #expect(request.params?["foo"]?.value as? Int == 42) 19 | } 20 | 21 | @Test 22 | func testDecodeJSONRPCResponse() throws { 23 | let json = """ 24 | {"jsonrpc":"2.0","id":1,"result":{"bar":"baz"}} 25 | """.data(using: .utf8)! 26 | let message = try JSONDecoder().decode(JSONRPCMessage.self, from: json) 27 | guard case .response(let response) = message else { 28 | throw TestError("Expected response case") 29 | } 30 | #expect(response.jsonrpc == "2.0") 31 | #expect(response.id == 1) 32 | #expect(response.result?["bar"]?.value as? String == "baz") 33 | } 34 | 35 | @Test 36 | func testDecodeJSONRPCErrorResponse() throws { 37 | let json = """ 38 | {"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}} 39 | """.data(using: .utf8)! 40 | let message = try JSONDecoder().decode(JSONRPCMessage.self, from: json) 41 | guard case .errorResponse(let error) = message else { 42 | throw TestError("Expected errorResponse case") 43 | } 44 | #expect(error.jsonrpc == "2.0") 45 | #expect(error.id == 1) 46 | #expect(error.error.code == -32601) 47 | #expect(error.error.message == "Method not found") 48 | } 49 | 50 | @Test 51 | func testDecodeJSONRPCBatch() throws { 52 | let json = """ 53 | [ 54 | {"jsonrpc":"2.0","id":1,"method":"ping"}, 55 | {"jsonrpc":"2.0","method":"notifications/initialized"}, 56 | {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}} 57 | ] 58 | """.data(using: .utf8)! 59 | 60 | let batch = try JSONDecoder().decode([JSONRPCMessage].self, from: json) 61 | #expect(batch.count == 3) 62 | 63 | // Check first message is a request 64 | guard case .request(let request1) = batch[0] else { 65 | throw TestError("Expected first message to be request") 66 | } 67 | #expect(request1.id == 1) 68 | #expect(request1.method == "ping") 69 | 70 | // Check second message is a notification 71 | guard case .notification(let notification) = batch[1] else { 72 | throw TestError("Expected second message to be notification") 73 | } 74 | #expect(notification.method == "notifications/initialized") 75 | 76 | // Check third message is a request 77 | guard case .request(let request2) = batch[2] else { 78 | throw TestError("Expected third message to be request") 79 | } 80 | #expect(request2.id == 2) 81 | #expect(request2.method == "tools/list") 82 | } 83 | 84 | @Test 85 | func testHandleBatchRequest() async throws { 86 | let calculator = Calculator() 87 | 88 | // Create a batch with mixed requests and notifications 89 | let batch: [JSONRPCMessage] = [ 90 | .request(id: 1, method: "ping"), 91 | .notification(method: "notifications/initialized"), 92 | .request(id: 2, method: "tools/list") 93 | ] 94 | 95 | var responses: [JSONRPCMessage] = [] 96 | 97 | // Process each message in the batch 98 | for message in batch { 99 | if let response = await calculator.handleMessage(message) { 100 | responses.append(response) 101 | } 102 | } 103 | 104 | // Should have 2 responses (ping and tools/list), notification has no response 105 | #expect(responses.count == 2) 106 | 107 | // Check first response (ping) 108 | guard case .response(let response1) = responses[0] else { 109 | throw TestError("Expected first response to be response") 110 | } 111 | #expect(response1.id == 1) 112 | 113 | // Check second response (tools/list) 114 | guard case .response(let response2) = responses[1] else { 115 | throw TestError("Expected second response to be response") 116 | } 117 | #expect(response2.id == 2) 118 | } 119 | 120 | @Test 121 | func testEncodeBatchResponse() throws { 122 | let responses: [JSONRPCMessage] = [ 123 | .response(id: 1, result: [:]), 124 | .response(id: 2, result: ["tools": AnyCodable([])]) 125 | ] 126 | 127 | let encoder = JSONEncoder() 128 | let data = try encoder.encode(responses) 129 | 130 | // Verify it's a valid JSON array 131 | let decoded = try JSONDecoder().decode([JSONRPCMessage].self, from: data) 132 | #expect(decoded.count == 2) 133 | 134 | // Check first response 135 | guard case .response(let response1) = decoded[0] else { 136 | throw TestError("Expected first message to be response") 137 | } 138 | #expect(response1.id == 1) 139 | 140 | // Check second response 141 | guard case .response(let response2) = decoded[1] else { 142 | throw TestError("Expected second message to be response") 143 | } 144 | #expect(response2.id == 2) 145 | } 146 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/MCPServerAutoConformanceTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | import SwiftMCP 3 | 4 | // Test class that uses the MCPServer macro without explicitly conforming to MCPServer 5 | @MCPServer 6 | class AutoConformingCalculator { 7 | /// Adds two integers and returns their sum 8 | /// - Parameter a: First number to add 9 | /// - Parameter b: Second number to add 10 | /// - Returns: The sum of a and b 11 | @MCPTool 12 | func add(a: Int, b: Int) -> Int { 13 | return a + b 14 | } 15 | } 16 | 17 | @Test("Auto Protocol Conformance") 18 | func testAutoProtocolConformance() async { 19 | // Create an instance of the class 20 | let calculator = AutoConformingCalculator() 21 | 22 | // Verify that it conforms to MCPServer by checking if mcpTools is available 23 | #expect(!calculator.mcpToolMetadata.isEmpty) 24 | 25 | // Verify that we can call a tool through the MCPServer protocol method 26 | do { 27 | let result = try await calculator.callTool("add", arguments: ["a": 2, "b": 3]) 28 | #expect(result as? Int == 5) 29 | } catch { 30 | #expect(Bool(false), "Should not throw an error: \(error)") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/MCPServerParametersTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | import SwiftMCP 3 | import Foundation 4 | import AnyCodable 5 | 6 | // Utility function to unwrap optionals in tests 7 | func unwrap(_ optional: T?, message: Comment = "Unexpected nil") -> T { 8 | #expect(optional != nil, message) 9 | return optional! 10 | } 11 | 12 | 13 | 14 | @MCPServer(name: "CustomCalculator", version: "2.0") 15 | final class CustomNameCalculator: MCPServer { 16 | @MCPTool(description: "Simple addition") 17 | func add(a: Int, b: Int) -> Int { 18 | return a + b 19 | } 20 | } 21 | 22 | @MCPServer 23 | final class DefaultNameCalculator: MCPServer { 24 | @MCPTool(description: "Simple addition") 25 | func add(a: Int, b: Int) -> Int { 26 | return a + b 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/MCPToolArgumentEnrichingTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import SwiftMCP 3 | 4 | /** 5 | This test suite verifies that the MCPTool macro correctly handles default values for parameters. 6 | 7 | It tests: 8 | 1. Parameters with default values are correctly marked as optional (not required) 9 | 2. Parameters with default values have the correct type in the JSON schema 10 | 3. Multiple parameters with default values in the same function are handled correctly 11 | */ 12 | 13 | @Test 14 | func testEnrichArguments() throws { 15 | let calculator = Calculator() 16 | 17 | // Get the add tool metadata from the calculator 18 | let metadata = unwrap(calculator.mcpToolMetadata(for: "add")) 19 | 20 | // Test enriching arguments 21 | let arguments: [String: Codable & Sendable] = ["a": 2, "b": 3] 22 | let enrichedArguments = try metadata.enrichArguments(arguments) 23 | 24 | // Check that the arguments were not changed 25 | #expect(enrichedArguments.count == 2) 26 | #expect(enrichedArguments["a"] as? Int == 2) 27 | #expect(enrichedArguments["b"] as? Int == 3) 28 | } 29 | 30 | @Test 31 | func testEnrichArgumentsWithExplicitFunctionName() throws { 32 | let calculator = Calculator() 33 | 34 | // Get the add tool metadata from the calculator 35 | let metadata = unwrap(calculator.mcpToolMetadata(for: "add")) 36 | 37 | // Test enriching arguments with explicit function name 38 | let arguments: [String: Codable & Sendable] = ["a": 2, "b": 3] 39 | let enrichedArguments = try metadata.enrichArguments(arguments) 40 | 41 | // Check that the arguments were not changed 42 | #expect(enrichedArguments.count == 2) 43 | #expect(enrichedArguments["a"] as? Int == 2) 44 | #expect(enrichedArguments["b"] as? Int == 3) 45 | } 46 | 47 | @Test 48 | func testEnrichArgumentsWithNoDefaults() throws { 49 | let calculator = Calculator() 50 | 51 | // Get the add tool metadata from the calculator 52 | let metadata = unwrap(calculator.mcpToolMetadata(for: "add")) 53 | 54 | // Test enriching arguments with no default values 55 | let arguments: [String: Codable & Sendable] = ["a": 2, "b": 3] 56 | let enrichedArguments = try metadata.enrichArguments(arguments) 57 | 58 | // Check that the arguments were not changed 59 | #expect(enrichedArguments.count == 2) 60 | #expect(enrichedArguments["a"] as? Int == 2) 61 | #expect(enrichedArguments["b"] as? Int == 3) 62 | } 63 | 64 | @Test 65 | func testEnrichArgumentsWithMissingRequiredArgument() throws { 66 | let calculator = Calculator() 67 | 68 | // Get the add tool metadata from the calculator 69 | let metadata = unwrap(calculator.mcpToolMetadata(for: "add")) 70 | 71 | // Test enriching arguments with a missing required argument 72 | #expect(throws: MCPToolError.self, "Should notice missing parameter") { 73 | try metadata.enrichArguments(["a": 2 as (Codable & Sendable)]) 74 | } 75 | } 76 | 77 | @Test 78 | func testEnrichArgumentsWithTypeConversion() throws { 79 | let calculator = Calculator() 80 | 81 | // Get the add tool metadata from the calculator 82 | let metadata = unwrap(calculator.mcpToolMetadata(for: "add")) 83 | 84 | // Test enriching arguments with string values that need to be converted 85 | let arguments: [String: Codable & Sendable] = ["a": "2", "b": "3"] 86 | let enrichedArguments = try metadata.enrichArguments(arguments) 87 | 88 | // Check that the arguments were not changed (enrichArguments doesn't do type conversion) 89 | #expect(enrichedArguments.count == 2) 90 | #expect(enrichedArguments["a"] as? String == "2") // String is not converted by enrichArguments 91 | #expect(enrichedArguments["b"] as? String == "3") // String is not converted by enrichArguments 92 | } 93 | 94 | @Test 95 | func testSubtractArguments() throws { 96 | let calculator = Calculator() 97 | 98 | // Get the subtract tool metadata from the calculator 99 | let metadata = unwrap(calculator.mcpToolMetadata(for: "subtract")) 100 | 101 | // Test with no arguments - should throw missing required parameter 102 | #expect(throws: MCPToolError.self, "Should notice missing parameter") { 103 | try metadata.enrichArguments([:]) 104 | } 105 | 106 | // Test with partial arguments - should throw missing required parameter 107 | #expect(throws: MCPToolError.self, "Should notice missing parameter") { 108 | try metadata.enrichArguments(["b": 5 as (Codable & Sendable)]) 109 | } 110 | 111 | // Test with all arguments - no defaults should be added 112 | let allArgs = try metadata.enrichArguments(["a": 20 as (Codable & Sendable), "b": 5 as (Codable & Sendable)]) 113 | #expect(allArgs.count == 2) 114 | #expect(allArgs["a"] as? Int == 20) 115 | #expect(allArgs["b"] as? Int == 5) 116 | } 117 | 118 | @Test 119 | func testMultiplyArguments() throws { 120 | let calculator = Calculator() 121 | 122 | // Get the multiply tool metadata from the calculator 123 | let metadata = unwrap(calculator.mcpToolMetadata(for: "multiply")) 124 | 125 | // Test with no arguments - should throw missing required parameter 126 | #expect(throws: MCPToolError.self, "Should notice missing parameter") { 127 | try metadata.enrichArguments([:]) 128 | } 129 | 130 | // Test with partial arguments - should throw missing required parameter 131 | #expect(throws: MCPToolError.self, "Should notice missing parameter") { 132 | try metadata.enrichArguments(["b": 5 as (Codable & Sendable)]) 133 | } 134 | 135 | // Test with all arguments - no defaults should be added 136 | let allArgs = try metadata.enrichArguments(["a": 20 as (Codable & Sendable), "b": 5 as (Codable & Sendable)]) 137 | #expect(allArgs.count == 2) 138 | #expect(allArgs["a"] as? Int == 20) 139 | #expect(allArgs["b"] as? Int == 5) 140 | } 141 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/MCPToolWarningTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import SwiftMCP 3 | 4 | /** 5 | This test suite verifies that the MCPTool macro correctly handles functions with missing descriptions. 6 | 7 | It tests: 8 | 1. Functions with parameter documentation but no function description 9 | 2. Functions with an explicit description parameter 10 | 3. Functions with a documentation comment containing a description 11 | */ 12 | 13 | // MARK: - Test Classes 14 | 15 | // Test class with functions missing descriptions 16 | @MCPServer 17 | final class MissingDescriptions { 18 | 19 | // Has documentation but no description line 20 | /// - Parameter a: A parameter 21 | @MCPTool 22 | func missingDescription(a: Int) {} 23 | 24 | // Has description parameter 25 | @MCPTool(description: "This function has a description parameter") 26 | func hasDescriptionParameter() {} 27 | 28 | // Has documentation comment with description 29 | /// This function has a documentation comment 30 | @MCPTool 31 | func hasDocumentationComment() {} 32 | } 33 | 34 | // MARK: - Tests 35 | 36 | @Test 37 | func testMissingDescriptions() throws { 38 | let instance = MissingDescriptions() 39 | 40 | // Get the tools array 41 | let tools = instance.mcpToolMetadata.convertedToTools() 42 | 43 | // Test function with parameter documentation but no function description 44 | guard let missingDescriptionTool = tools.first(where: { $0.name == "missingDescription" }) else { 45 | throw TestError("Could not find missingDescription function") 46 | } 47 | 48 | // The missingDescription function should have nil description (special case) 49 | #expect(missingDescriptionTool.description == nil, "Function with no description should have nil description") 50 | 51 | // Extract properties from the object schema 52 | if case .object(let object) = missingDescriptionTool.inputSchema { 53 | if case .number(let description) = object.properties["a"] { 54 | #expect(description == "A parameter") 55 | } else { 56 | #expect(Bool(false), "Expected number schema for parameter 'a'") 57 | } 58 | } else { 59 | #expect(Bool(false), "Expected object schema") 60 | } 61 | 62 | // Test function with description parameter 63 | guard let hasDescriptionParameterTool = tools.first(where: { $0.name == "hasDescriptionParameter" }) else { 64 | throw TestError("Could not find hasDescriptionParameter function") 65 | } 66 | 67 | #expect(hasDescriptionParameterTool.description == "This function has a description parameter") 68 | 69 | // Test function with documentation comment 70 | guard let hasDocumentationCommentTool = tools.first(where: { $0.name == "hasDocumentationComment" }) else { 71 | throw TestError("Could not find hasDocumentationComment function") 72 | } 73 | 74 | // Check if the description contains the expected text (may have additional comment markers) 75 | #expect(hasDocumentationCommentTool.description?.contains("This function has a documentation comment") == true) 76 | } 77 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/MockClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import SwiftMCP 3 | 4 | /// A generic mock client that can work with any MCPServer 5 | /// Simulates JSON serialization/deserialization like a real JSON-RPC client 6 | class MockClient { 7 | private let server: MCPServer 8 | 9 | init(server: MCPServer) { 10 | self.server = server 11 | } 12 | 13 | func send(_ request: JSONRPCMessage) async -> JSONRPCMessage? { 14 | // Get response from server 15 | let response = await server.handleMessage(request) 16 | 17 | // Simulate JSON round-trip to match real-world behavior 18 | guard let response = response else { return nil } 19 | 20 | do { 21 | // Encode to JSON like it would go over the wire 22 | let encoder = JSONEncoder() 23 | let jsonData = try encoder.encode(response) 24 | 25 | // Decode back like a client would receive it 26 | let decoder = JSONDecoder() 27 | let roundTripResponse = try decoder.decode(JSONRPCMessage.self, from: jsonData) 28 | 29 | return roundTripResponse 30 | } catch { 31 | print("MockClient JSON round-trip failed: \(error)") 32 | return response // Fallback to original if round-trip fails 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/NoOpLogHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoOpLogHandler.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 19.03.25. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | struct NoOpLogHandler: LogHandler { 12 | var logLevel: Logger.Level = .critical 13 | var metadata: Logger.Metadata = [:] 14 | 15 | subscript(metadataKey key: String) -> Logger.Metadata.Value? { 16 | get { return nil } 17 | set { } 18 | } 19 | 20 | func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt) { 21 | // Discard all log messages. 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/OpenAPIResourceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftMCP 3 | 4 | @MCPServer(name: "OpenAPITestServer", version: "1.0") 5 | class OpenAPITestServer { 6 | 7 | @MCPTool 8 | func testTool(message: String) -> String { 9 | return "Tool: \(message)" 10 | } 11 | 12 | @MCPResource("api://test/{id}") 13 | func testResource(id: Int) -> String { 14 | return "Resource: \(id)" 15 | } 16 | } 17 | 18 | final class OpenAPIResourceTests: XCTestCase { 19 | 20 | func testOpenAPISpecIncludesBothToolsAndResources() throws { 21 | let server = OpenAPITestServer() 22 | let spec = OpenAPISpec(server: server, scheme: "https", host: "example.com") 23 | 24 | 25 | // Should have 2 paths: one for the tool and one for the resource 26 | XCTAssertEqual(spec.paths.count, 2) 27 | 28 | // Check that both tool and resource are included 29 | let pathKeys = Set(spec.paths.keys) 30 | XCTAssertTrue(pathKeys.contains("/openapitestserver/testTool")) 31 | XCTAssertTrue(pathKeys.contains("/openapitestserver/testResource")) 32 | 33 | // Verify both have POST operations 34 | XCTAssertNotNil(spec.paths["/openapitestserver/testTool"]?.post) 35 | XCTAssertNotNil(spec.paths["/openapitestserver/testResource"]?.post) 36 | 37 | // Verify the resource function has the correct parameter 38 | let resourceOperation = spec.paths["/openapitestserver/testResource"]?.post 39 | XCTAssertNotNil(resourceOperation?.requestBody) 40 | 41 | // The resource should have an 'id' parameter 42 | if case let .object(inputSchema) = resourceOperation?.requestBody?.content["application/json"]?.schema { 43 | XCTAssertTrue(inputSchema.properties.keys.contains("id")) 44 | XCTAssertTrue(inputSchema.required.contains("id")) 45 | } else { 46 | XCTFail("Expected object schema for resource input") 47 | } 48 | } 49 | 50 | func testResourceFunctionCanBeCalledViaHTTPHandler() async throws { 51 | let server = OpenAPITestServer() 52 | 53 | // Test that the resource function can be called as a function 54 | let result = try await server.callResourceAsFunction("testResource", arguments: ["id": 42]) 55 | 56 | // Should return the resource content 57 | XCTAssertTrue(result is String || result is [GenericResourceContent]) 58 | 59 | if let stringResult = result as? String { 60 | XCTAssertEqual(stringResult, "Resource: 42") 61 | } else if let resourceArray = result as? [GenericResourceContent] { 62 | XCTAssertEqual(resourceArray.count, 1) 63 | XCTAssertEqual(resourceArray.first?.text, "Resource: 42") 64 | } else { 65 | XCTFail("Unexpected result type: \(type(of: result))") 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/PingTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import SwiftMCP 4 | import AnyCodable 5 | 6 | @Test("Ping Request") 7 | func testPingRequest() async throws { 8 | // Create a calculator instance 9 | let calculator = Calculator() 10 | 11 | // Create a ping request 12 | let pingRequest = JSONRPCMessage.request( 13 | id: 1, 14 | method: "ping" 15 | ) 16 | 17 | // Handle the request 18 | guard let message = await calculator.handleMessage(pingRequest) else { 19 | #expect(Bool(false), "Expected a response message") 20 | return 21 | } 22 | 23 | guard case .response(let response) = message else { 24 | #expect(Bool(false), "Expected response case") 25 | return 26 | } 27 | 28 | #expect(response.id == 1) 29 | } 30 | -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/TestError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestError.swift 3 | // SwiftMCP 4 | // 5 | // Created by Oliver Drobnik on 10.03.25. 6 | // 7 | 8 | 9 | struct TestError: Error { 10 | let message: String 11 | 12 | init(_ message: String) { 13 | self.message = message 14 | } 15 | } -------------------------------------------------------------------------------- /Tests/SwiftMCPTests/URIConstructionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftMCP 3 | 4 | final class URIConstructionTests: XCTestCase { 5 | 6 | func testSimpleVariableConstruction() throws { 7 | let template = "users://{user_id}/profile" 8 | let parameters: [String: Sendable] = ["user_id": "123"] 9 | 10 | let constructedURI = try template.constructURI(with: parameters) 11 | 12 | XCTAssertEqual(constructedURI.absoluteString, "users://123/profile") 13 | } 14 | 15 | func testQueryParameterConstruction() throws { 16 | let template = "users://{user_id}/profile?locale={lang}" 17 | let parameters: [String: Sendable] = ["user_id": "456", "lang": "fr"] 18 | 19 | let constructedURI = try template.constructURI(with: parameters) 20 | 21 | XCTAssertEqual(constructedURI.absoluteString, "users://456/profile?locale=fr") 22 | } 23 | 24 | func testMultipleSimpleVariables() throws { 25 | let template = "api://{version}/{resource}/{id}" 26 | let parameters: [String: Sendable] = ["version": "v1", "resource": "users", "id": "789"] 27 | 28 | let constructedURI = try template.constructURI(with: parameters) 29 | 30 | XCTAssertEqual(constructedURI.absoluteString, "api://v1/users/789") 31 | } 32 | 33 | func testReservedExpansion() throws { 34 | let template = "files://{+path}" 35 | let parameters: [String: Sendable] = ["path": "documents/folder/file.txt"] 36 | 37 | let constructedURI = try template.constructURI(with: parameters) 38 | 39 | XCTAssertEqual(constructedURI.absoluteString, "files://documents/folder/file.txt") 40 | } 41 | 42 | func testPathSegmentExpansion() throws { 43 | let template = "api://{/segments}" 44 | let parameters: [String: Sendable] = ["segments": "v1,users,123"] 45 | 46 | let constructedURI = try template.constructURI(with: parameters) 47 | 48 | XCTAssertEqual(constructedURI.absoluteString, "api:///v1/users/123") 49 | } 50 | 51 | func testLabelExpansion() throws { 52 | let template = "api://example.com{.format}" 53 | let parameters: [String: Sendable] = ["format": "json"] 54 | 55 | let constructedURI = try template.constructURI(with: parameters) 56 | 57 | XCTAssertEqual(constructedURI.absoluteString, "api://example.com.json") 58 | } 59 | 60 | func testFragmentExpansion() throws { 61 | let template = "page://document{#section}" 62 | let parameters: [String: Sendable] = ["section": "introduction"] 63 | 64 | let constructedURI = try template.constructURI(with: parameters) 65 | 66 | XCTAssertEqual(constructedURI.absoluteString, "page://document#introduction") 67 | } 68 | 69 | func testMissingOptionalParameter() throws { 70 | let template = "users://{user_id}/profile?locale={lang}" 71 | let parameters: [String: Sendable] = ["user_id": "123"] 72 | 73 | let constructedURI = try template.constructURI(with: parameters) 74 | 75 | // Should construct without the optional query parameter 76 | XCTAssertEqual(constructedURI.absoluteString, "users://123/profile") 77 | } 78 | 79 | func testIntegerParameter() throws { 80 | let template = "users://{user_id}/posts/{post_id}" 81 | let parameters: [String: Sendable] = ["user_id": 123, "post_id": 456] 82 | 83 | let constructedURI = try template.constructURI(with: parameters) 84 | 85 | XCTAssertEqual(constructedURI.absoluteString, "users://123/posts/456") 86 | } 87 | 88 | func testBooleanParameter() throws { 89 | let template = "settings://notifications?enabled={enabled}" 90 | let parameters: [String: Sendable] = ["enabled": true] 91 | 92 | let constructedURI = try template.constructURI(with: parameters) 93 | 94 | XCTAssertEqual(constructedURI.absoluteString, "settings://notifications?enabled=true") 95 | } 96 | } --------------------------------------------------------------------------------