Initial implementation

This commit is contained in:
Mattt Zmuda 2025-02-11 09:27:37 -08:00
commit be1e9583f8
No known key found for this signature in database
37 changed files with 4304 additions and 884 deletions

3
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["swiftlang.swift-vscode"]
}

8
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "swiftlang.swift-vscode",
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
}
}

201
LICENSE Normal file
View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 Loopwork Limited
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

24
Package.resolved Normal file
View file

@ -0,0 +1,24 @@
{
"originHash" : "cce36cb33302c2f1c28458e19b8439f736fc28106e4c6ea95d7992c74594c242",
"pins" : [
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91",
"version" : "1.6.2"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-system.git",
"state" : {
"revision" : "c8a44d836fe7913603e246acab7c528c2e780168",
"version" : "1.4.0"
}
}
],
"version" : 3
}

View file

@ -1,4 +1,4 @@
// swift-tools-version:5.7
// swift-tools-version:6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@ -6,26 +6,34 @@ import PackageDescription
let package = Package(
name: "mcp-swift-sdk",
platforms: [
.macOS(.v12),
.iOS(.v15),
.tvOS(.v15),
.watchOS(.v8)
.macOS(.v14),
.iOS(.v17),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "MCP",
targets: ["MCP"]),
targets: ["MCP"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-system.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
],
dependencies: [],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "MCP",
dependencies: []),
dependencies: [
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "Logging", package: "swift-log"),
]),
.testTarget(
name: "MCPTests",
dependencies: ["MCP"]),
dependencies: [
"MCP",
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "Logging", package: "swift-log"),
]),
]
)

145
README.md
View file

@ -1,23 +1,12 @@
# MCP Swift SDK
A Swift implementation of the Model Context Protocol (MCP) client. This SDK provides a modern, Swift-native way to interact with MCP servers.
## Features
- Full async/await support
- Type-safe API
- Resource management
- Tool integration
- Configurable transport layer
- Swift concurrency with actor-based design
Swift implementation of the [Model Context Protocol][mcp] (MCP).
## Requirements
- Swift 5.7+
- macOS 12.0+
- iOS 15.0+
- tvOS 15.0+
- watchOS 8.0+
- Swift 6.0+ / Xcode 16+
- macOS 14.0+ (Sonoma)
- iOS 17.0+
## Installation
@ -27,45 +16,65 @@ Add the following to your `Package.swift` file:
```swift
dependencies: [
.package(url: "https://github.com/yourusername/mcp-swift-sdk.git", from: "1.0.0")
.package(url: "https://github.com/loopwork-ai/mcp-swift-sdk.git", from: "0.1.0")
]
```
## Usage
### Basic Setup
### Basic Client Setup
```swift
import MCP
// Create a transport (e.g., StdioTransport)
let transport = StdioTransport(command: "/path/to/server")
// Create client info
let clientInfo = Implementation(name: "MyApp", version: "1.0.0")
// Initialize the client
let client = MCPClient(transport: transport, clientInfo: clientInfo)
let client = Client(name: "MyApp", version: "1.0.0")
// Connect and initialize
try await client.connect()
// Create a transport and connect
let transport = StdioTransport()
try await client.connect(transport: transport)
// Initialize the connection
let result = try await client.initialize()
```
### Working with Resources
### Basic Server Setup
```swift
// List available resources
let resources = try await client.listResources()
import MCP
// Read a resource
let resource = try await client.readResource("resource://example")
// Initialize the server with capabilities
let server = Server(
name: "MyServer",
version: "1.0.0",
capabilities: .init(
resources: .init(
list: true,
read: true,
subscribe: true
)
)
)
// Subscribe to resource updates
try await client.subscribeResource("resource://example")
// Create transport and start server
let transport = StdioTransport()
try await server.start(transport: transport)
// Unsubscribe from resource updates
try await client.unsubscribeResource("resource://example")
// Register method handlers
server.withMethodHandler(ReadResource.self) { params in
// Handle resource read request
let uri = params.uri
let content = [Resource.Content.text("Example content")]
return .init(contents: content)
}
// Register notification handlers
server.onNotification(ResourceUpdatedNotification.self) { message in
// Handle resource update notification
}
// Stop the server when done
await server.stop()
```
### Working with Tools
@ -75,41 +84,57 @@ try await client.unsubscribeResource("resource://example")
let tools = try await client.listTools()
// Call a tool
let result = try await client.callTool(name: "example-tool", arguments: ["key": "value"])
```
let (content, isError) = try await client.callTool(
name: "example-tool",
arguments: ["key": "value"]
)
### Custom Transport
You can create your own transport by implementing the `MCPTransport` protocol:
```swift
public protocol MCPTransport {
func connect() async throws
func disconnect() async
func sendRequest(_ data: Data) async throws -> Data
func sendNotification(_ data: Data) async throws
// Handle tool content
for item in content {
switch item {
case .text(let text):
print(text)
case .image(let data, let mimeType, let metadata):
// Handle image data
}
}
```
## Error Handling
The SDK uses the `MCPError` type for error handling:
### Working with Resources
```swift
public enum MCPError: LocalizedError {
case connectionClosed
case invalidResponse
case invalidParams(String)
case serverError(String)
case protocolError(String)
case transportError(Error)
// List available resources
let (resources, nextCursor) = try await client.listResources()
// Read a resource
let contents = try await client.readResource(uri: "resource://example")
// Subscribe to resource updates
try await client.subscribeToResource(uri: "resource://example")
// Handle resource updates
await client.onNotification(ResourceUpdatedNotification.self) { message in
let uri = message.params.uri
let content = message.params.content
// Handle the update
}
```
## Contributing
### Working with Prompts
Contributions are welcome! Please feel free to submit a Pull Request.
```swift
// List available prompts
let (prompts, nextCursor) = try await client.listPrompts()
// Get a prompt with arguments
let (description, messages) = try await client.getPrompt(
name: "example-prompt",
arguments: ["key": "value"]
)
```
## License
This project is licensed under the MIT License - see the LICENSE file for details.
This project is licensed under the Apache License, Version 2.0.
[mcp]: https://modelcontextprotocol.io

View file

@ -0,0 +1,212 @@
import Foundation
/// A model context protocol error.
public enum Error: Sendable {
// Standard JSON-RPC 2.0 errors (-32700 to -32603)
case parseError(String?) // -32700
case invalidRequest(String?) // -32600
case methodNotFound(String?) // -32601
case invalidParams(String?) // -32602
case internalError(String?) // -32603
// Server errors (-32000 to -32099)
case serverError(code: Int, message: String)
// Transport specific errors
case connectionClosed
case transportError(Swift.Error)
/// The JSON-RPC 2.0 error code
public var code: Int {
switch self {
case .parseError: return -32700
case .invalidRequest: return -32600
case .methodNotFound: return -32601
case .invalidParams: return -32602
case .internalError: return -32603
case .serverError(let code, _): return code
case .connectionClosed: return -32000
case .transportError: return -32001
}
}
}
// MARK: LocalizedError
extension Error: LocalizedError {
public var errorDescription: String? {
switch self {
case .parseError(let detail):
return "Parse error: Invalid JSON" + (detail.map { ": \($0)" } ?? "")
case .invalidRequest(let detail):
return "Invalid Request" + (detail.map { ": \($0)" } ?? "")
case .methodNotFound(let detail):
return "Method not found" + (detail.map { ": \($0)" } ?? "")
case .invalidParams(let detail):
return "Invalid params" + (detail.map { ": \($0)" } ?? "")
case .internalError(let detail):
return "Internal error" + (detail.map { ": \($0)" } ?? "")
case .serverError(_, let message):
return "Server error: \(message)"
case .connectionClosed:
return "Connection closed"
case .transportError(let error):
return "Transport error: \(error.localizedDescription)"
}
}
public var failureReason: String? {
switch self {
case .parseError:
return "The server received invalid JSON that could not be parsed"
case .invalidRequest:
return "The JSON sent is not a valid Request object"
case .methodNotFound:
return "The method does not exist or is not available"
case .invalidParams:
return "Invalid method parameter(s)"
case .internalError:
return "Internal JSON-RPC error"
case .serverError:
return "Server-defined error occurred"
case .connectionClosed:
return "The connection to the server was closed"
case .transportError(let error):
return (error as? LocalizedError)?.failureReason ?? error.localizedDescription
}
}
public var recoverySuggestion: String? {
switch self {
case .parseError:
return "Verify that the JSON being sent is valid and well-formed"
case .invalidRequest:
return "Ensure the request follows the JSON-RPC 2.0 specification format"
case .methodNotFound:
return "Check the method name and ensure it is supported by the server"
case .invalidParams:
return "Verify the parameters match the method's expected parameters"
case .connectionClosed:
return "Try reconnecting to the server"
default:
return nil
}
}
}
// MARK: CustomDebugStringConvertible
extension Error: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .transportError(let error):
return
"[\(code)] \(errorDescription ?? "") (Underlying error: \(String(reflecting: error)))"
default:
return "[\(code)] \(errorDescription ?? "")"
}
}
}
// MARK: Codable
extension Error: Codable {
private enum CodingKeys: String, CodingKey {
case code, message, data
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(code, forKey: .code)
try container.encode(errorDescription ?? "Unknown error", forKey: .message)
// Encode additional data if available
switch self {
case .parseError(let detail),
.invalidRequest(let detail),
.methodNotFound(let detail),
.invalidParams(let detail),
.internalError(let detail):
if let detail = detail {
try container.encode(["detail": detail], forKey: .data)
}
case .serverError(_, _):
// No additional data for server errors
break
case .connectionClosed:
break
case .transportError(let error):
try container.encode(
["error": error.localizedDescription],
forKey: .data
)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let code = try container.decode(Int.self, forKey: .code)
let message = try container.decode(String.self, forKey: .message)
let data = try container.decodeIfPresent([String: Value].self, forKey: .data)
switch code {
case -32700:
self = .parseError(data?["detail"] as? String ?? message)
case -32600:
self = .invalidRequest(data?["detail"] as? String ?? message)
case -32601:
self = .methodNotFound(data?["detail"] as? String ?? message)
case -32602:
self = .invalidParams(data?["detail"] as? String ?? message)
case -32603:
self = .internalError(data?["detail"] as? String ?? message)
case -32000:
self = .connectionClosed
case -32001:
self = .transportError(
NSError(
domain: "org.jsonrpc.error",
code: code,
userInfo: [NSLocalizedDescriptionKey: message]
)
)
default:
self = .serverError(code: code, message: message)
}
}
}
// MARK: Equatable
extension Error: Equatable {
public static func == (lhs: Error, rhs: Error) -> Bool {
lhs.code == rhs.code
}
}
// MARK: Hashable
extension Error: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(code)
switch self {
case .parseError(let detail):
hasher.combine(detail)
case .invalidRequest(let detail):
hasher.combine(detail)
case .methodNotFound(let detail):
hasher.combine(detail)
case .invalidParams(let detail):
hasher.combine(detail)
case .internalError(let detail):
hasher.combine(detail)
case .serverError(_, let message):
hasher.combine(message)
case .connectionClosed:
break
case .transportError(let error):
hasher.combine(error.localizedDescription)
}
}
}

69
Sources/MCP/Base/ID.swift Normal file
View file

@ -0,0 +1,69 @@
import struct Foundation.UUID
/// A unique identifier for a request.
public enum ID: Hashable, Sendable {
/// A string ID.
case string(String)
/// A number ID.
case number(Int)
/// Generates a random string ID.
public static var random: ID {
return .string(UUID().uuidString)
}
}
// MARK: - ExpressibleByStringLiteral
extension ID: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = .string(value)
}
}
// MARK: - ExpressibleByIntegerLiteral
extension ID: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self = .number(value)
}
}
// MARK: - CustomStringConvertible
extension ID: CustomStringConvertible {
public var description: String {
switch self {
case .string(let str): return str
case .number(let num): return String(num)
}
}
}
// MARK: - Codable
extension ID: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let number = try? container.decode(Int.self) {
self = .number(number)
} else if container.decodeNil() {
// Handle unspecified/null IDs as empty string
self = .string("")
} else {
throw DecodingError.dataCorruptedError(
in: container, debugDescription: "ID must be string or number")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let str): try container.encode(str)
case .number(let num): try container.encode(num)
}
}
}

View file

@ -0,0 +1,56 @@
/// The initialization phase MUST be the first interaction between client and server.
/// During this phase, the client and server:
/// - Establish protocol version compatibility
/// - Exchange and negotiate capabilities
/// - Share implementation details
///
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/#initialization
public enum Initialize: Method {
public static let name: String = "initialize"
public struct Parameters: Hashable, Codable, Sendable {
public let protocolVersion: String
public let capabilities: Client.Capabilities
public let clientInfo: Client.Info
public init(
protocolVersion: String = Version.latest,
capabilities: Client.Capabilities,
clientInfo: Client.Info
) {
self.protocolVersion = protocolVersion
self.capabilities = capabilities
self.clientInfo = clientInfo
}
private enum CodingKeys: String, CodingKey {
case protocolVersion, capabilities, clientInfo
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
protocolVersion =
try container.decodeIfPresent(String.self, forKey: .protocolVersion)
?? Version.latest
capabilities =
try container.decodeIfPresent(Client.Capabilities.self, forKey: .capabilities)
?? .init()
clientInfo =
try container.decodeIfPresent(Client.Info.self, forKey: .clientInfo)
?? .init(name: "unknown", version: "0.0.0")
}
}
public struct Result: Hashable, Codable, Sendable {
public let protocolVersion: String
public let capabilities: Server.Capabilities
public let serverInfo: Server.Info
public let instructions: String?
}
}
/// After successful initialization, the client MUST send an initialized notification to indicate it is ready to begin normal operations.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/#initialization
public struct InitializedNotification: Notification {
public static let name: String = "notifications/initialized"
}

View file

@ -0,0 +1,332 @@
import Foundation
private let jsonrpc = "2.0"
public struct Empty: Hashable, Codable, Sendable {}
// MARK: -
/// A method that can be used to send requests and receive responses.
public protocol Method {
/// The parameters of the method.
associatedtype Parameters: Codable, Hashable, Sendable = Empty
/// The result of the method.
associatedtype Result: Codable, Hashable, Sendable = Empty
/// The name of the method.
static var name: String { get }
}
/// Type-erased method for request/response handling
struct AnyMethod: Method {
static var name: String { "" }
typealias Parameters = Value
typealias Result = Value
}
extension Method where Parameters == Empty {
public static func request(id: ID = .random) -> Request<Self> {
Request(id: id, method: name, params: Empty())
}
}
extension Method where Result == Empty {
public static func response(id: ID) -> Response<Self> {
Response(id: id, result: Empty())
}
}
extension Method {
/// Create a request with the given parameters.
public static func request(id: ID = .random, _ parameters: Self.Parameters) -> Request<Self> {
Request(id: id, method: name, params: parameters)
}
/// Create a response with the given result.
public static func response(id: ID, result: Self.Result) -> Response<Self> {
Response(id: id, result: result)
}
/// Create a response with the given error.
public static func response(id: ID, error: Error) -> Response<Self> {
Response(id: id, error: error)
}
}
// MARK: -
/// A request message.
public struct Request<M: Method>: Hashable, Identifiable, Codable, Sendable {
/// The request ID.
public let id: ID
/// The method name.
public let method: String
/// The request parameters.
public let params: M.Parameters
init(id: ID = .random, method: String, params: M.Parameters) {
self.id = id
self.method = method
self.params = params
}
private enum CodingKeys: String, CodingKey {
case jsonrpc, id, method, params
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(jsonrpc, forKey: .jsonrpc)
try container.encode(id, forKey: .id)
try container.encode(method, forKey: .method)
if M.Parameters.self != Empty.self {
try container.encode(params, forKey: .params)
} else {
// Encode empty object for Empty parameters
try container.encode(Empty(), forKey: .params)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let version = try container.decode(String.self, forKey: .jsonrpc)
guard version == jsonrpc else {
throw DecodingError.dataCorruptedError(
forKey: .jsonrpc, in: container, debugDescription: "Invalid JSON-RPC version")
}
id = try container.decode(ID.self, forKey: .id)
method = try container.decode(String.self, forKey: .method)
if M.Parameters.self == Empty.self {
if (try? container.decodeNil(forKey: .params)) != nil {
params = Empty() as! M.Parameters
} else if (try? container.decode(Empty.self, forKey: .params)) != nil {
params = Empty() as! M.Parameters
} else {
// If params field is missing, use Empty
params = Empty() as! M.Parameters
}
} else {
params = try container.decode(M.Parameters.self, forKey: .params)
}
}
}
/// A type-erased request for request/response handling
typealias AnyRequest = Request<AnyMethod>
/// A box for request handlers that can be type-erased
class RequestHandlerBox: @unchecked Sendable {
func callAsFunction(_ request: Request<AnyMethod>) async throws -> Response<AnyMethod> {
fatalError("Must override")
}
}
/// A typed request handler that can be used to handle requests of a specific type
final class TypedRequestHandler<M: Method>: RequestHandlerBox, @unchecked Sendable {
private let _handle: @Sendable (Request<M>) async throws -> Response<M>
init(_ handler: @escaping @Sendable (Request<M>) async throws -> Response<M>) {
self._handle = handler
super.init()
}
override func callAsFunction(_ request: Request<AnyMethod>) async throws -> Response<AnyMethod>
{
// Create a concrete request from the type-erased one
let data = try JSONEncoder().encode(request.params)
let params = try JSONDecoder().decode(M.Parameters.self, from: data)
let typedRequest = Request<M>(id: request.id, method: M.name, params: params)
// Handle with concrete type
let response = try await _handle(typedRequest)
// Convert result to AnyMethod response
switch response.result {
case .success(let result):
let resultData = try JSONEncoder().encode(result)
let resultValue = try JSONDecoder().decode(Value.self, from: resultData)
return Response(id: response.id, result: resultValue)
case .failure(let error):
return Response(id: response.id, error: error)
}
}
}
// MARK: -
/// A response message.
public struct Response<M: Method>: Hashable, Identifiable, Codable, Sendable {
/// The response ID.
public let id: ID
/// The response result.
public let result: Swift.Result<M.Result, Error>
public init(id: ID, result: M.Result) {
self.id = id
self.result = .success(result)
}
public init(id: ID, error: Error) {
self.id = id
self.result = .failure(error)
}
private enum CodingKeys: String, CodingKey {
case jsonrpc, id, result, error
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(jsonrpc, forKey: .jsonrpc)
try container.encode(id, forKey: .id)
switch result {
case .success(let result):
try container.encode(result, forKey: .result)
case .failure(let error):
try container.encode(error, forKey: .error)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let version = try container.decode(String.self, forKey: .jsonrpc)
guard version == jsonrpc else {
throw DecodingError.dataCorruptedError(
forKey: .jsonrpc, in: container, debugDescription: "Invalid JSON-RPC version")
}
id = try container.decode(ID.self, forKey: .id)
if let result = try? container.decode(M.Result.self, forKey: .result) {
self.result = .success(result)
} else if let error = try? container.decode(Error.self, forKey: .error) {
self.result = .failure(error)
} else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: container.codingPath,
debugDescription: "Invalid response"))
}
}
}
/// A type-erased response for request/response handling
typealias AnyResponse = Response<AnyMethod>
// MARK: -
/// A notification message.
public protocol Notification {
/// The parameters of the notification.
associatedtype Parameters: Hashable, Codable, Sendable = Empty
/// The name of the notification.
static var name: String { get }
}
/// A type-erased notification for message handling
struct AnyNotification: Notification {
static var name: String { "" }
typealias Parameters = Empty
}
/// A message that can be used to send notifications.
public struct Message<N: Notification>: Hashable, Codable, Sendable {
/// The method name.
public let method: String
/// The notification parameters.
public let params: N.Parameters
public init(method: String, params: N.Parameters) {
self.method = method
self.params = params
}
private enum CodingKeys: String, CodingKey {
case jsonrpc, method, params
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(jsonrpc, forKey: .jsonrpc)
try container.encode(method, forKey: .method)
if N.Parameters.self != Empty.self {
try container.encode(params, forKey: .params)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let version = try container.decode(String.self, forKey: .jsonrpc)
guard version == jsonrpc else {
throw DecodingError.dataCorruptedError(
forKey: .jsonrpc, in: container, debugDescription: "Invalid JSON-RPC version")
}
method = try container.decode(String.self, forKey: .method)
// Handle params field being optional
if N.Parameters.self == Empty.self {
// For Empty parameters, use Empty() if params is missing or null
params = Empty() as! N.Parameters
} else if let value = try? container.decode(N.Parameters.self, forKey: .params) {
// If params exists and can be decoded, use it
params = value
} else if !container.contains(.params)
|| (try? container.decodeNil(forKey: .params)) == true
{
// If params is missing or explicitly null, use Empty for Empty parameters
// or throw for non-Empty parameters
if N.Parameters.self == Empty.self {
params = Empty() as! N.Parameters
} else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: container.codingPath,
debugDescription: "Missing required params field"))
}
} else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: container.codingPath,
debugDescription: "Invalid params field"))
}
}
}
/// A type-erased message for message handling
typealias AnyMessage = Message<AnyNotification>
extension Notification where Parameters == Empty {
/// Create a message with empty parameters.
public static func message() -> Message<Self> {
Message(method: name, params: Empty())
}
}
extension Notification {
/// Create a message with the given parameters.
public static func message(_ parameters: Parameters) -> Message<Self> {
Message(method: name, params: parameters)
}
}
/// A box for notification handlers that can be type-erased
class NotificationHandlerBox: @unchecked Sendable {
func callAsFunction(_ notification: Message<AnyNotification>) async throws {}
}
/// A typed notification handler that can be used to handle notifications of a specific type
final class TypedNotificationHandler<N: Notification>: NotificationHandlerBox,
@unchecked Sendable
{
private let _handle: @Sendable (Message<N>) async throws -> Void
init(_ handler: @escaping @Sendable (Message<N>) async throws -> Void) {
self._handle = handler
super.init()
}
override func callAsFunction(_ notification: Message<AnyNotification>) async throws {
// Create a concrete notification from the type-erased one
let data = try JSONEncoder().encode(notification)
let typedNotification = try JSONDecoder().decode(Message<N>.self, from: data)
try await _handle(typedNotification)
}
}

View file

@ -0,0 +1,172 @@
import Darwin
import Logging
import SystemPackage
import struct Foundation.Data
import struct Foundation.Date
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
/// Protocol defining the transport layer for MCP communication
public protocol Transport: Actor {
var logger: Logger { get }
/// Establishes connection with the transport
func connect() async throws
/// Disconnects from the transport
func disconnect() async
/// Sends a message string
func send(_ message: String) async throws
/// Receives message strings as an async sequence
func receive() -> AsyncThrowingStream<String, Swift.Error>
}
/// Standard input/output transport implementation
public actor StdioTransport: Transport {
private let input: FileDescriptor
private let output: FileDescriptor
public nonisolated let logger: Logger
private var isConnected = false
private let messageStream: AsyncStream<String>
private let messageContinuation: AsyncStream<String>.Continuation
public init(
input: FileDescriptor = FileDescriptor.standardInput,
output: FileDescriptor = FileDescriptor.standardOutput,
logger: Logger? = nil
) {
self.input = input
self.output = output
self.logger =
logger
?? Logger(
label: "mcp.transport.stdio",
factory: { _ in SwiftLogNoOpLogHandler() })
// Create message stream
var continuation: AsyncStream<String>.Continuation!
self.messageStream = AsyncStream { continuation = $0 }
self.messageContinuation = continuation
}
public func connect() async throws {
guard !isConnected else { return }
// Set non-blocking mode
try setNonBlocking(fileDescriptor: input)
try setNonBlocking(fileDescriptor: output)
isConnected = true
logger.info("Transport connected successfully")
// Start reading loop in background
Task {
await readLoop()
}
}
private func setNonBlocking(fileDescriptor: FileDescriptor) throws {
let flags = fcntl(fileDescriptor.rawValue, F_GETFL)
guard flags >= 0 else {
throw Error.transportError(Errno.badFileDescriptor)
}
let result = fcntl(fileDescriptor.rawValue, F_SETFL, flags | O_NONBLOCK)
guard result >= 0 else {
throw Error.transportError(Errno.badFileDescriptor)
}
}
private func readLoop() async {
let bufferSize = 4096
var buffer = [UInt8](repeating: 0, count: bufferSize)
var pendingData = Data()
while isConnected && !Task.isCancelled {
do {
let bytesRead = try buffer.withUnsafeMutableBufferPointer { pointer in
try input.read(into: UnsafeMutableRawBufferPointer(pointer))
}
if bytesRead == 0 {
logger.notice("EOF received")
break
}
pendingData.append(Data(buffer[..<bytesRead]))
// Process complete messages
while let newlineIndex = pendingData.firstIndex(of: UInt8(ascii: "\n")) {
let messageData = pendingData[..<newlineIndex]
pendingData = pendingData[(newlineIndex + 1)...]
if let message = String(data: messageData, encoding: .utf8),
!message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
logger.debug("Message received", metadata: ["message": "\(message)"])
messageContinuation.yield(message)
}
}
} catch let error as Errno where error == .resourceTemporarilyUnavailable {
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms backoff
continue
} catch {
if !Task.isCancelled {
logger.error("Read error occurred", metadata: ["error": "\(error)"])
}
break
}
}
messageContinuation.finish()
}
public func disconnect() async {
guard isConnected else { return }
isConnected = false
messageContinuation.finish()
logger.info("Transport disconnected")
}
public func send(_ message: String) async throws {
guard isConnected else {
throw Error.transportError(Errno.socketNotConnected)
}
let message = message + "\n"
guard let data = message.data(using: .utf8) else {
throw Error.transportError(Errno.invalidArgument)
}
var remaining = data
while !remaining.isEmpty {
do {
let written = try remaining.withUnsafeBytes { buffer in
try output.write(UnsafeRawBufferPointer(buffer))
}
if written > 0 {
remaining = remaining.dropFirst(written)
}
} catch let error as Errno where error == .resourceTemporarilyUnavailable {
try await Task.sleep(nanoseconds: 10_000_000) // 10ms backoff
continue
} catch {
throw Error.transportError(error)
}
}
}
public func receive() -> AsyncThrowingStream<String, Swift.Error> {
return AsyncThrowingStream { continuation in
Task {
for await message in messageStream {
continuation.yield(message)
}
continuation.finish()
}
}
}
}

View file

@ -0,0 +1,5 @@
/// The Model Context Protocol includes an optional ping mechanism that allows either party to verify that their counterpart is still responsive and the connection is alive.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/utilities/ping
public enum Ping: Method {
public static let name: String = "ping"
}

View file

@ -0,0 +1,402 @@
import struct Foundation.Data
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
/// A codable value.
public enum Value: Hashable, Sendable {
case null
case bool(Bool)
case int(Int)
case double(Double)
case string(String)
case data(mimeType: String? = nil, Data)
case array([Value])
case object([String: Value])
/// Create a `Value` from a `Codable` value.
/// - Parameter value: The codable value
/// - Returns: A value
public init<T: Codable>(_ value: T) throws {
if let valueAsValue = value as? Value {
self = valueAsValue
} else {
let data = try JSONEncoder().encode(value)
self = try JSONDecoder().decode(Value.self, from: data)
}
}
/// Returns whether the value is `null`.
public var isNull: Bool {
return self == .null
}
/// Returns the `Bool` value if the value is a `bool`,
/// otherwise returns `nil`.
public var boolValue: Bool? {
guard case let .bool(value) = self else { return nil }
return value
}
/// Returns the `Int` value if the value is an `integer`,
/// otherwise returns `nil`.
public var intValue: Int? {
guard case let .int(value) = self else { return nil }
return value
}
/// Returns the `Double` value if the value is a `double`,
/// otherwise returns `nil`.
public var doubleValue: Double? {
guard case let .double(value) = self else { return nil }
return value
}
/// Returns the `String` value if the value is a `string`,
/// otherwise returns `nil`.
public var stringValue: String? {
guard case let .string(value) = self else { return nil }
return value
}
/// Returns the data value and optional MIME type if the value is `data`,
/// otherwise returns `nil`.
public var dataValue: (mimeType: String?, Data)? {
guard case let .data(mimeType: mimeType, data) = self else { return nil }
return (mimeType: mimeType, data)
}
/// Returns the `[Value]` value if the value is an `array`,
/// otherwise returns `nil`.
public var arrayValue: [Value]? {
guard case let .array(value) = self else { return nil }
return value
}
/// Returns the `[String: Value]` value if the value is an `object`,
/// otherwise returns `nil`.
public var objectValue: [String: Value]? {
guard case let .object(value) = self else { return nil }
return value
}
}
// MARK: - Codable
extension Value: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
} else if let value = try? container.decode(Bool.self) {
self = .bool(value)
} else if let value = try? container.decode(Int.self) {
self = .int(value)
} else if let value = try? container.decode(Double.self) {
self = .double(value)
} else if let value = try? container.decode(String.self) {
if Data.isDataURL(string: value),
case let (mimeType, data)? = Data.parseDataURL(value)
{
self = .data(mimeType: mimeType, data)
} else {
self = .string(value)
}
} else if let value = try? container.decode([Value].self) {
self = .array(value)
} else if let value = try? container.decode([String: Value].self) {
self = .object(value)
} else {
throw DecodingError.dataCorruptedError(
in: container, debugDescription: "Value type not found")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .null:
try container.encodeNil()
case .bool(let value):
try container.encode(value)
case .int(let value):
try container.encode(value)
case .double(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
case let .data(mimeType, value):
try container.encode(value.dataURLEncoded(mimeType: mimeType))
case .array(let value):
try container.encode(value)
case .object(let value):
try container.encode(value)
}
}
}
extension Value: CustomStringConvertible {
public var description: String {
switch self {
case .null:
return ""
case .bool(let value):
return value.description
case .int(let value):
return value.description
case .double(let value):
return value.description
case .string(let value):
return value.description
case let .data(mimeType, value):
return value.dataURLEncoded(mimeType: mimeType)
case .array(let value):
return value.description
case .object(let value):
return value.description
}
}
}
// MARK: - ExpressibleByNilLiteral
extension Value: ExpressibleByNilLiteral {
public init(nilLiteral: ()) {
self = .null
}
}
// MARK: - ExpressibleByBooleanLiteral
extension Value: ExpressibleByBooleanLiteral {
public init(booleanLiteral value: Bool) {
self = .bool(value)
}
}
// MARK: - ExpressibleByIntegerLiteral
extension Value: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self = .int(value)
}
}
// MARK: - ExpressibleByFloatLiteral
extension Value: ExpressibleByFloatLiteral {
public init(floatLiteral value: Double) {
self = .double(value)
}
}
// MARK: - ExpressibleByStringLiteral
extension Value: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = .string(value)
}
}
// MARK: - ExpressibleByArrayLiteral
extension Value: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: Value...) {
self = .array(elements)
}
}
// MARK: - ExpressibleByDictionaryLiteral
extension Value: ExpressibleByDictionaryLiteral {
public init(dictionaryLiteral elements: (String, Value)...) {
var dictionary: [String: Value] = [:]
for (key, value) in elements {
dictionary[key] = value
}
self = .object(dictionary)
}
}
// MARK: - ExpressibleByStringInterpolation
extension Value: ExpressibleByStringInterpolation {
public struct StringInterpolation: StringInterpolationProtocol {
var stringValue: String
public init(literalCapacity: Int, interpolationCount: Int) {
self.stringValue = ""
self.stringValue.reserveCapacity(literalCapacity + interpolationCount)
}
public mutating func appendLiteral(_ literal: String) {
self.stringValue.append(literal)
}
public mutating func appendInterpolation<T: CustomStringConvertible>(_ value: T) {
self.stringValue.append(value.description)
}
}
public init(stringInterpolation: StringInterpolation) {
self = .string(stringInterpolation.stringValue)
}
}
// MARK: - Standard Library Type Extensions
extension Bool {
/// Creates a boolean value from a `Value` instance.
///
/// In strict mode, only `.bool` values are converted. In non-strict mode, the following conversions are supported:
/// - Integers: `1` is `true`, `0` is `false`
/// - Doubles: `1.0` is `true`, `0.0` is `false`
/// - Strings (lowercase only):
/// - `true`: "true", "t", "yes", "y", "on", "1"
/// - `false`: "false", "f", "no", "n", "off", "0"
///
/// - Parameters:
/// - value: The `Value` to convert
/// - strict: When `true`, only converts from `.bool` values. Defaults to `true`
/// - Returns: A boolean value if conversion is possible, `nil` otherwise
///
/// - Example:
/// ```swift
/// Bool(Value.bool(true)) // Returns true
/// Bool(Value.int(1), strict: false) // Returns true
/// Bool(Value.string("yes"), strict: false) // Returns true
/// ```
public init?(_ value: Value, strict: Bool = true) {
switch value {
case .bool(let b):
self = b
case .int(let i) where !strict:
switch i {
case 0: self = false
case 1: self = true
default: return nil
}
case .double(let d) where !strict:
switch d {
case 0.0: self = false
case 1.0: self = true
default: return nil
}
case .string(let s) where !strict:
switch s {
case "true", "t", "yes", "y", "on", "1":
self = true
case "false", "f", "no", "n", "off", "0":
self = false
default:
return nil
}
default:
return nil
}
}
}
extension Int {
/// Creates an integer value from a `Value` instance.
///
/// In strict mode, only `.int` values are converted. In non-strict mode, the following conversions are supported:
/// - Doubles: Converted if they can be represented exactly as integers
/// - Strings: Parsed if they contain a valid integer representation
///
/// - Parameters:
/// - value: The `Value` to convert
/// - strict: When `true`, only converts from `.int` values. Defaults to `true`
/// - Returns: An integer value if conversion is possible, `nil` otherwise
///
/// - Example:
/// ```swift
/// Int(Value.int(42)) // Returns 42
/// Int(Value.double(42.0), strict: false) // Returns 42
/// Int(Value.string("42"), strict: false) // Returns 42
/// Int(Value.double(42.5), strict: false) // Returns nil
/// ```
public init?(_ value: Value, strict: Bool = true) {
switch value {
case .int(let i):
self = i
case .double(let d) where !strict:
guard let intValue = Int(exactly: d) else { return nil }
self = intValue
case .string(let s) where !strict:
guard let intValue = Int(s) else { return nil }
self = intValue
default:
return nil
}
}
}
extension Double {
/// Creates a double value from a `Value` instance.
///
/// In strict mode, converts from `.double` and `.int` values. In non-strict mode, the following conversions are supported:
/// - Integers: Converted to their double representation
/// - Strings: Parsed if they contain a valid floating-point representation
///
/// - Parameters:
/// - value: The `Value` to convert
/// - strict: When `true`, only converts from `.double` and `.int` values. Defaults to `true`
/// - Returns: A double value if conversion is possible, `nil` otherwise
///
/// - Example:
/// ```swift
/// Double(Value.double(42.5)) // Returns 42.5
/// Double(Value.int(42)) // Returns 42.0
/// Double(Value.string("42.5"), strict: false) // Returns 42.5
/// ```
public init?(_ value: Value, strict: Bool = true) {
switch value {
case .double(let d):
self = d
case .int(let i):
self = Double(i)
case .string(let s) where !strict:
guard let doubleValue = Double(s) else { return nil }
self = doubleValue
default:
return nil
}
}
}
extension String {
/// Creates a string value from a `Value` instance.
///
/// In strict mode, only `.string` values are converted. In non-strict mode, the following conversions are supported:
/// - Integers: Converted to their string representation
/// - Doubles: Converted to their string representation
/// - Booleans: Converted to "true" or "false"
///
/// - Parameters:
/// - value: The `Value` to convert
/// - strict: When `true`, only converts from `.string` values. Defaults to `true`
/// - Returns: A string value if conversion is possible, `nil` otherwise
///
/// - Example:
/// ```swift
/// String(Value.string("hello")) // Returns "hello"
/// String(Value.int(42), strict: false) // Returns "42"
/// String(Value.bool(true), strict: false) // Returns "true"
/// ```
public init?(_ value: Value, strict: Bool = true) {
switch value {
case .string(let s):
self = s
case .int(let i) where !strict:
self = String(i)
case .double(let d) where !strict:
self = String(d)
case .bool(let b) where !strict:
self = String(b)
default:
return nil
}
}
}

View file

@ -0,0 +1,11 @@
import Foundation
/// The Model Context Protocol uses string-based version identifiers
/// following the format YYYY-MM-DD, to indicate
/// the last date backwards incompatible changes were made.
///
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/versioning/
public enum Version {
/// The current protocol version.
public static let latest = "2024-11-05"
}

View file

@ -0,0 +1,377 @@
import Logging
import SystemPackage
import struct Foundation.Data
import struct Foundation.Date
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
/// Model Context Protocol client
public actor Client {
/// Implementation information
public struct Info: Hashable, Codable, Sendable {
/// The client name
public var name: String
/// The client version
public var version: String
public init(name: String, version: String) {
self.name = name
self.version = version
}
}
/// The client capabilities
public struct Capabilities: Hashable, Codable, Sendable {
/// The roots capabilities
public struct Roots: Hashable, Codable, Sendable {
/// Whether the list of roots has changed
public var listChanged: Bool?
public init(listChanged: Bool? = nil) {
self.listChanged = listChanged
}
}
/// The sampling capabilities
public struct Sampling: Hashable, Codable, Sendable {
public init() {}
}
/// Whether the client supports sampling
public var sampling: Sampling?
/// Experimental features supported by the client
public var experimental: [String: String]?
/// Whether the client supports roots
public var roots: Capabilities.Roots?
public init(
sampling: Sampling? = nil,
experimental: [String: String]? = nil,
roots: Capabilities.Roots? = nil
) {
self.sampling = sampling
self.experimental = experimental
self.roots = roots
}
}
/// The connection to the server
private var connection: (any Transport)?
/// The logger for the client
private var logger: Logger? {
get async {
await connection?.logger
}
}
/// The client information
private let clientInfo: Client.Info
/// The client name
public nonisolated var name: String { clientInfo.name }
/// The client version
public nonisolated var version: String { clientInfo.version }
/// The client capabilities
public var capabilities: Client.Capabilities
/// The server capabilities
private var serverCapabilities: Server.Capabilities?
/// The server version
private var serverVersion: String?
/// The server instructions
private var instructions: String?
/// A dictionary of type-erased notification handlers, keyed by method name
private var notificationHandlers: [String: [NotificationHandlerBox]] = [:]
/// The task for the message handling loop
private var task: Task<Void, Never>?
/// A pending request with a continuation for the result
private struct PendingRequest<T> {
let continuation: CheckedContinuation<T, Swift.Error>
}
/// A dictionary of type-erased pending requests, keyed by request ID
private var pendingRequests: [ID: Any] = [:]
public init(
name: String,
version: String
) {
self.clientInfo = Client.Info(name: name, version: version)
self.capabilities = Capabilities()
}
/// Connect to the server using the given transport
public func connect(transport: any Transport) async throws {
self.connection = transport
try await self.connection?.connect()
await logger?.info(
"Client connected", metadata: ["name": "\(name)", "version": "\(version)"])
// Start message handling loop
task = Task {
guard let connection = self.connection else { return }
repeat {
// Check for cancellation before starting the iteration
if Task.isCancelled { break }
do {
let stream = await connection.receive()
for try await string in stream {
if Task.isCancelled { break } // Check inside loop too
// Decode and handle incoming message
guard let data = string.data(using: .utf8) else {
throw Error.parseError("Invalid UTF-8 data")
}
// Attempt to decode string data as AnyResponse or AnyMessage
let decoder = JSONDecoder()
if let response = try? decoder.decode(AnyResponse.self, from: data) {
await handleResponse(response, for: response)
} else if let message = try? decoder.decode(AnyMessage.self, from: data) {
await handleMessage(message)
} else {
var metadata: Logger.Metadata = [:]
if let string = String(data: data, encoding: .utf8) {
metadata["message"] = .string(string)
}
await logger?.warning(
"Unexpected message received by client", metadata: metadata)
}
}
} catch let error as Errno where error == .resourceTemporarilyUnavailable {
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
continue
} catch {
await logger?.error(
"Error in message handling loop", metadata: ["error": "\(error)"])
break
}
} while true
}
}
/// Disconnect the client and cancel all pending requests
public func disconnect() async {
// Cancel all pending requests
for (id, request) in pendingRequests {
// We know this cast is safe because we only store PendingRequest values
if let typedRequest = request as? PendingRequest<Any> {
typedRequest.continuation.resume(
throwing: Error.internalError("Client disconnected"))
}
pendingRequests.removeValue(forKey: id)
}
task?.cancel()
task = nil
if let connection = connection {
await connection.disconnect()
}
connection = nil
}
// MARK: - Registration
/// Register a handler for a notification
@discardableResult
public func onNotification<N: Notification>(
_ type: N.Type,
handler: @escaping @Sendable (Message<N>) async throws -> Void
) async -> Self {
let handlers = notificationHandlers[N.name, default: []]
notificationHandlers[N.name] = handlers + [TypedNotificationHandler(handler)]
return self
}
// MARK: - Requests
/// Send a request and receive its response
public func send<M: Method>(_ request: Request<M>) async throws -> M.Result {
guard let connection = connection else {
throw Error.internalError("Client connection not initialized")
}
let requestData = try JSONEncoder().encode(request)
guard let requestString = String(data: requestData, encoding: .utf8) else {
throw Error.internalError("Failed to encode request")
}
return try await withCheckedThrowingContinuation { continuation in
// Store the pending request first
Task {
self.addPendingRequest(
id: request.id,
continuation: continuation,
type: M.Result.self
)
// Send the request
do {
try await connection.send(requestString)
} catch {
continuation.resume(throwing: error)
self.removePendingRequest(id: request.id)
}
}
}
}
private func addPendingRequest<T>(
id: ID,
continuation: CheckedContinuation<T, Swift.Error>,
type: T.Type
) {
pendingRequests[id] = PendingRequest(continuation: continuation)
}
private func removePendingRequest(id: ID) {
pendingRequests.removeValue(forKey: id)
}
// MARK: - Lifecycle
public func initialize() async throws -> Initialize.Result {
let request = Initialize.request(
.init(
protocolVersion: Version.latest,
capabilities: capabilities,
clientInfo: clientInfo
))
let result = try await send(request)
self.serverCapabilities = result.capabilities
self.serverVersion = result.protocolVersion
self.instructions = result.instructions
return result
}
public func ping() async throws {
let request = Ping.request()
_ = try await send(request)
}
// MARK: - Prompts
public func getPrompt(name: String, arguments: [String: Value]? = nil) async throws
-> (description: String?, messages: [Prompt.Message])
{
_ = try checkCapability(\.prompts, "Prompts")
let request = GetPrompt.request(.init(name: name, arguments: arguments))
let result = try await send(request)
return (description: result.description, messages: result.messages)
}
public func listPrompts(cursor: String? = nil) async throws
-> (prompts: [Prompt], nextCursor: String?)
{
_ = try checkCapability(\.prompts, "Prompts")
let request = ListPrompts.request(.init(cursor: cursor))
let result = try await send(request)
return (prompts: result.prompts, nextCursor: result.nextCursor)
}
// MARK: - Resources
public func readResource(uri: String) async throws -> [Resource.Content] {
_ = try checkCapability(\.resources?.read, "Resource reading")
let request = ReadResource.request(.init(uri: uri))
let result = try await send(request)
return result.contents
}
public func listResources(cursor: String? = nil) async throws -> (
resources: [Resource], nextCursor: String?
) {
_ = try checkCapability(\.resources?.list, "Resource listing")
let request = ListResources.request(.init(cursor: cursor))
let result = try await send(request)
return (resources: result.resources, nextCursor: result.nextCursor)
}
public func subscribeToResource(uri: String) async throws {
_ = try checkCapability(\.resources?.subscribe, "Resource subscription")
let request = ResourceSubscribe.request(.init(uri: uri))
_ = try await send(request)
}
// MARK: - Tools
public func listTools(cursor: String? = nil) async throws -> [Tool] {
_ = try checkCapability(\.tools, "Tools")
let request = ListTools.request(.init(cursor: cursor))
let result = try await send(request)
return result.tools
}
public func callTool(name: String, arguments: [String: Value]? = nil) async throws -> (
content: [Tool.Content], isError: Bool
) {
_ = try checkCapability(\.tools, "Tools")
let request = CallTool.request(.init(name: name, arguments: arguments))
let result = try await send(request)
return (content: result.content, isError: result.isError)
}
// MARK: -
private func handleResponse(_ response: Response<AnyMethod>, for request: Any) async {
await logger?.debug(
"Processing response",
metadata: ["id": "\(response.id)"])
// We know this cast is safe because we only store PendingRequest values
guard let typedRequest = request as? PendingRequest<Any> else { return }
switch response.result {
case .success(let value):
typedRequest.continuation.resume(returning: value)
case .failure(let error):
typedRequest.continuation.resume(throwing: error)
}
removePendingRequest(id: response.id)
}
private func handleMessage(_ message: Message<AnyNotification>) async {
await logger?.debug(
"Processing notification",
metadata: ["method": "\(message.method)"])
// Find notification handlers for this method
guard let handlers = notificationHandlers[message.method] else { return }
// Convert notification parameters to concrete type and call handlers
for handler in handlers {
do {
try await handler(message)
} catch {
await logger?.error(
"Error handling notification",
metadata: [
"method": "\(message.method)",
"error": "\(error)",
])
}
}
}
// MARK: -
private func checkCapability<T>(_ keyPath: KeyPath<Server.Capabilities, T?>, _ name: String)
throws -> T
{
guard let capabilities = serverCapabilities else {
throw Error.methodNotFound("Server capabilities not initialized")
}
guard let value = capabilities[keyPath: keyPath] else {
throw Error.methodNotFound("\(name) is not supported by the server")
}
return value
}
}

View file

@ -0,0 +1,87 @@
import Foundation
import RegexBuilder
extension Data {
/// Regex pattern for data URLs
@inline(__always) private static var dataURLRegex:
Regex<(Substring, Substring, Substring?, Substring)>
{
Regex {
"data:"
Capture {
ZeroOrMore(.reluctant) {
CharacterClass.anyOf(",;").inverted
}
}
Optionally {
";charset="
Capture {
OneOrMore(.reluctant) {
CharacterClass.anyOf(",;").inverted
}
}
}
Optionally { ";base64" }
","
Capture {
ZeroOrMore { .any }
}
}
}
/// Checks if a given string is a valid data URL.
///
/// - Parameter string: The string to check.
/// - Returns: `true` if the string is a valid data URL, otherwise `false`.
/// - SeeAlso: [RFC 2397](https://www.rfc-editor.org/rfc/rfc2397.html)
public static func isDataURL(string: String) -> Bool {
return string.wholeMatch(of: dataURLRegex) != nil
}
/// Parses a data URL string into its MIME type and data components.
///
/// - Parameter string: The data URL string to parse.
/// - Returns: A tuple containing the MIME type and decoded data, or `nil` if parsing fails.
/// - SeeAlso: [RFC 2397](https://www.rfc-editor.org/rfc/rfc2397.html)
public static func parseDataURL(_ string: String) -> (mimeType: String, data: Data)? {
guard let match = string.wholeMatch(of: dataURLRegex) else {
return nil
}
// Extract components using strongly typed captures
let (_, mediatype, charset, encodedData) = match.output
let isBase64 = string.contains(";base64,")
// Process MIME type
var mimeType = mediatype.isEmpty ? "text/plain" : String(mediatype)
if let charset = charset, !charset.isEmpty, mimeType.starts(with: "text/") {
mimeType += ";charset=\(charset)"
}
// Decode data
let decodedData: Data
if isBase64 {
guard let base64Data = Data(base64Encoded: String(encodedData)) else { return nil }
decodedData = base64Data
} else {
guard
let percentDecodedData = String(encodedData).removingPercentEncoding?.data(
using: .utf8)
else { return nil }
decodedData = percentDecodedData
}
return (mimeType: mimeType, data: decodedData)
}
/// Encodes the data as a data URL string with an optional MIME type.
///
/// - Parameter mimeType: The MIME type of the data. If `nil`, "text/plain" will be used.
/// - Returns: A data URL string representation of the data.
/// - SeeAlso: [RFC 2397](https://www.rfc-editor.org/rfc/rfc2397.html)
public func dataURLEncoded(mimeType: String? = nil) -> String {
let base64Data = self.base64EncodedString()
return "data:\(mimeType ?? "text/plain");base64,\(base64Data)"
}
}

View file

@ -1,364 +0,0 @@
import Foundation
/// Protocol version constants
public enum MCPVersion {
public static let latest = "1.0.0"
public static let supported = ["1.0.0"]
}
/// MCP error types
public enum MCPError: LocalizedError {
case connectionClosed
case invalidResponse
case invalidParams(String)
case serverError(String)
case protocolError(String)
case transportError(Error)
public var errorDescription: String? {
switch self {
case .connectionClosed:
return "Connection closed"
case .invalidResponse:
return "Invalid response from server"
case .invalidParams(let message):
return "Invalid parameters: \(message)"
case .serverError(let message):
return "Server error: \(message)"
case .protocolError(let message):
return "Protocol error: \(message)"
case .transportError(let error):
return "Transport error: \(error.localizedDescription)"
}
}
}
/// Base protocol for MCP messages
public protocol MCPMessage: Codable {
var jsonrpc: String { get }
}
/// Protocol for MCP requests
public protocol MCPRequest: MCPMessage {
var id: String { get }
var method: String { get }
}
/// Protocol for MCP notifications
public protocol MCPNotification: MCPMessage {
var method: String { get }
}
/// Protocol for MCP responses
public protocol MCPResponse: MCPMessage {
var id: String { get }
}
/// Implementation information
public struct Implementation: Codable {
public let name: String
public let version: String
public init(name: String, version: String) {
self.name = name
self.version = version
}
}
/// Client capabilities
public struct ClientCapabilities: Codable {
public var sampling: Bool?
public var experimental: [String: [String: Any]]?
public var roots: RootsCapability?
public init(sampling: Bool? = nil,
experimental: [String: [String: Any]]? = nil,
roots: RootsCapability? = nil) {
self.sampling = sampling
self.experimental = experimental
self.roots = roots
}
enum CodingKeys: String, CodingKey {
case sampling
case experimental
case roots
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
sampling = try container.decodeIfPresent(Bool.self, forKey: .sampling)
experimental = try container.decodeIfPresent([String: [String: Any]].self, forKey: .experimental)
roots = try container.decodeIfPresent(RootsCapability.self, forKey: .roots)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(sampling, forKey: .sampling)
try container.encodeIfPresent(experimental, forKey: .experimental)
try container.encodeIfPresent(roots, forKey: .roots)
}
}
/// Roots capability configuration
public struct RootsCapability: Codable {
public var listChanged: Bool?
public init(listChanged: Bool? = nil) {
self.listChanged = listChanged
}
}
/// Server capabilities
public struct ServerCapabilities: Codable {
public var logging: Bool?
public var prompts: Bool?
public var resources: ResourcesCapability?
public var tools: Bool?
public init(logging: Bool? = nil,
prompts: Bool? = nil,
resources: ResourcesCapability? = nil,
tools: Bool? = nil) {
self.logging = logging
self.prompts = prompts
self.resources = resources
self.tools = tools
}
}
/// Resources capability configuration
public struct ResourcesCapability: Codable {
public var subscribe: Bool?
public init(subscribe: Bool? = nil) {
self.subscribe = subscribe
}
}
/// Initialize request parameters
public struct InitializeRequestParams: Codable {
public let protocolVersion: String
public let capabilities: ClientCapabilities
public let clientInfo: Implementation
public init(protocolVersion: String = MCPVersion.latest,
capabilities: ClientCapabilities,
clientInfo: Implementation) {
self.protocolVersion = protocolVersion
self.capabilities = capabilities
self.clientInfo = clientInfo
}
}
/// Initialize request
public struct InitializeRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "initialize"
public let params: InitializeRequestParams
public init(id: String = UUID().uuidString, params: InitializeRequestParams) {
self.id = id
self.params = params
}
}
/// Initialize result
public struct InitializeResult: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: InitializeResultData
public struct InitializeResultData: Codable {
public let protocolVersion: String
public let capabilities: ServerCapabilities
public let serverInfo: Implementation
public let instructions: String?
}
}
/// MCP Client protocol
public protocol MCPClientProtocol {
func connect() async throws
func disconnect() async
func initialize() async throws -> InitializeResult
func ping() async throws
// Resource methods
func listResources() async throws -> ListResourcesResponse
func readResource(_ uri: String) async throws -> ReadResourceResponse
func subscribeResource(_ uri: String) async throws
func unsubscribeResource(_ uri: String) async throws
// Tool methods
func listTools() async throws -> ListToolsResponse
func callTool(name: String, arguments: [String: Any]? = nil) async throws -> CallToolResponse
}
/// MCP Client implementation
public actor MCPClient: MCPClientProtocol {
private let transport: MCPTransport
private let clientInfo: Implementation
private var capabilities: ClientCapabilities
private var serverCapabilities: ServerCapabilities?
private var serverVersion: Implementation?
private var instructions: String?
public init(transport: MCPTransport,
clientInfo: Implementation,
capabilities: ClientCapabilities = ClientCapabilities()) {
self.transport = transport
self.clientInfo = clientInfo
self.capabilities = capabilities
}
public func connect() async throws {
try await transport.connect()
}
public func disconnect() async {
await transport.disconnect()
}
public func initialize() async throws -> InitializeResult {
let request = InitializeRequest(params: InitializeRequestParams(
capabilities: capabilities,
clientInfo: clientInfo
))
let response: InitializeResult = try await sendRequest(request)
// Validate protocol version
guard MCPVersion.supported.contains(response.result.protocolVersion) else {
throw MCPError.protocolError("Unsupported protocol version: \(response.result.protocolVersion)")
}
// Store server information
serverCapabilities = response.result.capabilities
serverVersion = response.result.serverInfo
instructions = response.result.instructions
// Send initialized notification
try await sendNotification(InitializedNotification())
return response
}
public func ping() async throws {
let request = PingRequest()
try await sendRequest(request)
}
// MARK: - Resource Methods
public func listResources() async throws -> ListResourcesResponse {
guard serverCapabilities?.resources != nil else {
throw MCPError.protocolError("Server does not support resources")
}
let request = ListResourcesRequest()
return try await sendRequest(request)
}
public func readResource(_ uri: String) async throws -> ReadResourceResponse {
guard serverCapabilities?.resources != nil else {
throw MCPError.protocolError("Server does not support resources")
}
let request = ReadResourceRequest(uri: uri)
return try await sendRequest(request)
}
public func subscribeResource(_ uri: String) async throws {
guard let resources = serverCapabilities?.resources,
resources.subscribe == true else {
throw MCPError.protocolError("Server does not support resource subscriptions")
}
let request = SubscribeRequest(uri: uri)
try await sendRequest(request)
}
public func unsubscribeResource(_ uri: String) async throws {
guard let resources = serverCapabilities?.resources,
resources.subscribe == true else {
throw MCPError.protocolError("Server does not support resource subscriptions")
}
let request = UnsubscribeRequest(uri: uri)
try await sendRequest(request)
}
// MARK: - Tool Methods
public func listTools() async throws -> ListToolsResponse {
guard serverCapabilities?.tools == true else {
throw MCPError.protocolError("Server does not support tools")
}
let request = ListToolsRequest()
return try await sendRequest(request)
}
public func callTool(name: String, arguments: [String: Any]? = nil) async throws -> CallToolResponse {
guard serverCapabilities?.tools == true else {
throw MCPError.protocolError("Server does not support tools")
}
let request = CallToolRequest(name: name, arguments: arguments)
return try await sendRequest(request)
}
private func sendRequest<T: MCPRequest, U: MCPResponse>(_ request: T) async throws -> U {
let data = try JSONEncoder().encode(request)
let responseData = try await transport.sendRequest(data)
return try JSONDecoder().decode(U.self, from: responseData)
}
private func sendNotification<T: MCPNotification>(_ notification: T) async throws {
let data = try JSONEncoder().encode(notification)
try await transport.sendNotification(data)
}
// MARK: - Public Properties
/// Get the server's capabilities after initialization
public var serverCapabilitiesAfterInit: ServerCapabilities? {
serverCapabilities
}
/// Get the server's version information after initialization
public var serverVersionAfterInit: Implementation? {
serverVersion
}
/// Get the server's instructions after initialization
public var serverInstructions: String? {
instructions
}
}
/// MCP Transport protocol
public protocol MCPTransport {
func connect() async throws
func disconnect() async
func sendRequest(_ data: Data) async throws -> Data
func sendNotification(_ data: Data) async throws
}
/// Basic notification types
public struct InitializedNotification: MCPNotification {
public let jsonrpc: String = "2.0"
public let method: String = "notifications/initialized"
}
public struct PingRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "ping"
public init(id: String = UUID().uuidString) {
self.id = id
}
}

View file

@ -1,166 +0,0 @@
import Foundation
/// Resource metadata
public struct ResourceMetadata: Codable {
public var name: String
public var uri: String
public var metadata: [String: String]?
public init(name: String, uri: String, metadata: [String: String]? = nil) {
self.name = name
self.uri = uri
self.metadata = metadata
}
}
/// Resource content types
public enum ResourceContent: Codable {
case text(String)
case binary(Data)
case embedded([EmbeddedResource])
private enum CodingKeys: String, CodingKey {
case type
case content
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "text":
let content = try container.decode(String.self, forKey: .content)
self = .text(content)
case "binary":
let content = try container.decode(Data.self, forKey: .content)
self = .binary(content)
case "embedded":
let content = try container.decode([EmbeddedResource].self, forKey: .content)
self = .embedded(content)
default:
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown resource type")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text(let content):
try container.encode("text", forKey: .type)
try container.encode(content, forKey: .content)
case .binary(let content):
try container.encode("binary", forKey: .type)
try container.encode(content, forKey: .content)
case .embedded(let content):
try container.encode("embedded", forKey: .type)
try container.encode(content, forKey: .content)
}
}
}
/// Embedded resource type
public struct EmbeddedResource: Codable {
public var type: String
public var data: String
public var metadata: [String: String]?
public init(type: String, data: String, metadata: [String: String]? = nil) {
self.type = type
self.data = data
self.metadata = metadata
}
}
/// Resource request types
public struct ListResourcesRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "resources/list"
public init(id: String = UUID().uuidString) {
self.id = id
}
}
public struct ReadResourceRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "resources/read"
public let params: ReadResourceParams
public struct ReadResourceParams: Codable {
public let uri: String
}
public init(id: String = UUID().uuidString, uri: String) {
self.id = id
self.params = ReadResourceParams(uri: uri)
}
}
public struct SubscribeRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "resources/subscribe"
public let params: SubscribeParams
public struct SubscribeParams: Codable {
public let uri: String
}
public init(id: String = UUID().uuidString, uri: String) {
self.id = id
self.params = SubscribeParams(uri: uri)
}
}
public struct UnsubscribeRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "resources/unsubscribe"
public let params: UnsubscribeParams
public struct UnsubscribeParams: Codable {
public let uri: String
}
public init(id: String = UUID().uuidString, uri: String) {
self.id = id
self.params = UnsubscribeParams(uri: uri)
}
}
/// Resource response types
public struct ListResourcesResponse: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: ListResourcesResult
public struct ListResourcesResult: Codable {
public let resources: [ResourceMetadata]
}
}
public struct ReadResourceResponse: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: ReadResourceResult
public struct ReadResourceResult: Codable {
public let content: ResourceContent
}
}
/// Resource notification types
public struct ResourceUpdatedNotification: MCPNotification {
public let jsonrpc: String = "2.0"
public let method: String = "notifications/resource/updated"
public let params: ResourceUpdatedParams
public struct ResourceUpdatedParams: Codable {
public let uri: String
public let content: ResourceContent
}
}

View file

@ -0,0 +1,206 @@
import Foundation
/// The Model Context Protocol (MCP) provides a standardized way
/// for servers to expose prompt templates to clients.
/// Prompts allow servers to provide structured messages and instructions
/// for interacting with language models.
/// Clients can discover available prompts, retrieve their contents,
/// and provide arguments to customize them.
///
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/
public struct Prompt: Hashable, Codable, Sendable {
/// The prompt name
public let name: String
/// The prompt description
public let description: String?
/// The prompt arguments
public let arguments: [Argument]?
public init(name: String, description: String? = nil, arguments: [Argument]? = nil) {
self.name = name
self.description = description
self.arguments = arguments
}
/// An argument for a prompt
public struct Argument: Hashable, Codable, Sendable {
/// The argument name
public let name: String
/// The argument description
public let description: String?
/// Whether the argument is required
public let required: Bool?
public init(name: String, description: String? = nil, required: Bool? = nil) {
self.name = name
self.description = description
self.required = required
}
}
/// A message in a prompt
public struct Message: Hashable, Codable, Sendable {
/// The message role
public enum Role: String, Hashable, Codable, Sendable {
/// A user message
case user
/// An assistant message
case assistant
}
/// The message role
public let role: Role
/// The message content
public let content: Content
public init(role: Role, content: Content) {
self.role = role
self.content = content
}
/// Content types for messages
public enum Content: Hashable, Codable, Sendable {
/// Text content
case text(text: String)
/// Image content
case image(data: String, mimeType: String)
/// Embedded resource content
case resource(uri: String, mimeType: String, text: String?, blob: String?)
private enum CodingKeys: String, CodingKey {
case type, text, data, mimeType, uri, blob
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text(let text):
try container.encode("text", forKey: .type)
try container.encode(text, forKey: .text)
case .image(let data, let mimeType):
try container.encode("image", forKey: .type)
try container.encode(data, forKey: .data)
try container.encode(mimeType, forKey: .mimeType)
case .resource(let uri, let mimeType, let text, let blob):
try container.encode("resource", forKey: .type)
try container.encode(uri, forKey: .uri)
try container.encode(mimeType, forKey: .mimeType)
try container.encodeIfPresent(text, forKey: .text)
try container.encodeIfPresent(blob, forKey: .blob)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "text":
let text = try container.decode(String.self, forKey: .text)
self = .text(text: text)
case "image":
let data = try container.decode(String.self, forKey: .data)
let mimeType = try container.decode(String.self, forKey: .mimeType)
self = .image(data: data, mimeType: mimeType)
case "resource":
let uri = try container.decode(String.self, forKey: .uri)
let mimeType = try container.decode(String.self, forKey: .mimeType)
let text = try container.decodeIfPresent(String.self, forKey: .text)
let blob = try container.decodeIfPresent(String.self, forKey: .blob)
self = .resource(uri: uri, mimeType: mimeType, text: text, blob: blob)
default:
throw DecodingError.dataCorruptedError(
forKey: .type,
in: container,
debugDescription: "Unknown content type")
}
}
}
}
/// Reference type for prompts
public struct Reference: Hashable, Codable, Sendable {
/// The prompt reference name
public let name: String
public init(name: String) {
self.name = name
}
private enum CodingKeys: String, CodingKey {
case type, name
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode("ref/prompt", forKey: .type)
try container.encode(name, forKey: .name)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
_ = try container.decode(String.self, forKey: .type)
name = try container.decode(String.self, forKey: .name)
}
}
}
// MARK: -
/// To retrieve available prompts, clients send a `prompts/list` request.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/#listing-prompts
public enum ListPrompts: Method {
public static let name: String = "prompts/list"
public struct Parameters: Hashable, Codable, Sendable {
public let cursor: String?
public init(cursor: String? = nil) {
self.cursor = cursor
}
}
public struct Result: Hashable, Codable, Sendable {
public let prompts: [Prompt]
public let nextCursor: String?
public init(prompts: [Prompt], nextCursor: String? = nil) {
self.prompts = prompts
self.nextCursor = nextCursor
}
}
}
/// To retrieve a specific prompt, clients send a `prompts/get` request.
/// Arguments may be auto-completed through the completion API.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/#getting-a-prompt
public enum GetPrompt: Method {
public static let name: String = "prompts/get"
public struct Parameters: Hashable, Codable, Sendable {
public let name: String
public let arguments: [String: Value]?
public init(name: String, arguments: [String: Value]? = nil) {
self.name = name
self.arguments = arguments
}
}
public struct Result: Hashable, Codable, Sendable {
public let description: String?
public let messages: [Prompt.Message]
public init(description: String?, messages: [Prompt.Message]) {
self.description = description
self.messages = messages
}
}
}
/// When the list of available prompts changes, servers that declared the listChanged capability SHOULD send a notification.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/#list-changed-notification
public struct PromptListChangedNotification: Notification {
public static let name: String = "notifications/prompts/list_changed"
}

View file

@ -0,0 +1,195 @@
import Foundation
/// The Model Context Protocol (MCP) provides a standardized way
/// for servers to expose resources to clients.
/// Resources allow servers to share data that provides context to language models,
/// such as files, database schemas, or application-specific information.
/// Each resource is uniquely identified by a URI.
///
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/
public struct Resource: Hashable, Codable, Sendable {
/// The resource name
public var name: String
/// The resource URI
public var uri: String
/// The resource description
public var description: String?
/// The resource MIME type
public var mimeType: String?
/// The resource metadata
public var metadata: [String: String]?
public init(
name: String,
uri: String,
description: String? = nil,
mimeType: String? = nil,
metadata: [String: String]? = nil
) {
self.name = name
self.uri = uri
self.description = description
self.mimeType = mimeType
self.metadata = metadata
}
/// Content of a resource.
public struct Content: Hashable, Codable, Sendable {
/// The resource URI
public let uri: String
/// The resource MIME type
public let mimeType: String?
/// The resource text content
public let text: String?
/// The resource binary content
public let blob: String?
public static func text(_ content: String, uri: String, mimeType: String? = nil) -> Self {
.init(uri: uri, mimeType: mimeType, text: content)
}
public static func binary(_ data: Data, uri: String, mimeType: String? = nil) -> Self {
.init(uri: uri, mimeType: mimeType, blob: data.base64EncodedString())
}
private init(uri: String, mimeType: String? = nil, text: String? = nil) {
self.uri = uri
self.mimeType = mimeType
self.text = text
self.blob = nil
}
private init(uri: String, mimeType: String? = nil, blob: String) {
self.uri = uri
self.mimeType = mimeType
self.text = nil
self.blob = blob
}
}
/// A resource template.
public struct Template: Hashable, Codable, Sendable {
/// The URI template pattern
public var uriTemplate: String
/// The template name
public var name: String
/// The template description
public var description: String?
/// The resource MIME type
public var mimeType: String?
public init(
uriTemplate: String,
name: String,
description: String? = nil,
mimeType: String? = nil
) {
self.uriTemplate = uriTemplate
self.name = name
self.description = description
self.mimeType = mimeType
}
}
}
// MARK: -
/// To discover available resources, clients send a `resources/list` request.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#listing-resources
public enum ListResources: Method {
public static let name: String = "resources/list"
public struct Parameters: Hashable, Codable, Sendable {
public let cursor: String?
public init(cursor: String? = nil) {
self.cursor = cursor
}
}
public struct Result: Hashable, Codable, Sendable {
public let resources: [Resource]
public let nextCursor: String?
public init(resources: [Resource], nextCursor: String? = nil) {
self.resources = resources
self.nextCursor = nextCursor
}
}
}
/// To retrieve resource contents, clients send a `resources/read` request:
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#reading-resources
public enum ReadResource: Method {
public static let name: String = "resources/read"
public struct Parameters: Hashable, Codable, Sendable {
public let uri: String
public init(uri: String) {
self.uri = uri
}
}
public struct Result: Hashable, Codable, Sendable {
public let contents: [Resource.Content]
public init(contents: [Resource.Content]) {
self.contents = contents
}
}
}
/// To discover available resource templates, clients send a `resources/templates/list` request.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#resource-templates
public enum ListResourceTemplates: Method {
public static let name: String = "resources/templates/list"
public typealias Parameters = Empty
public struct Result: Hashable, Codable, Sendable {
public let templates: [Resource.Template]
public init(templates: [Resource.Template]) {
self.templates = templates
}
private enum CodingKeys: String, CodingKey {
case templates = "resourceTemplates"
}
}
}
/// When the list of available resources changes, servers that declared the listChanged capability SHOULD send a notification.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#list-changed-notification
public struct ResourceListChangedNotification: Notification {
public static let name: String = "notifications/resources/list_changed"
public typealias Parameters = Empty
}
/// Clients can subscribe to specific resources and receive notifications when they change.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#subscriptions
public enum ResourceSubscribe: Method {
public static let name: String = "resources/subscribe"
public struct Parameters: Hashable, Codable, Sendable {
public let uri: String
}
public typealias Result = Empty
}
/// When a resource changes, servers that declared the updated capability SHOULD send a notification to subscribed clients.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#subscriptions
public struct ResourceUpdatedNotification: Notification {
public static let name: String = "notifications/resources/updated"
public struct Parameters: Hashable, Codable, Sendable {
public let uri: String
public init(uri: String) {
self.uri = uri
}
}
}

Some files were not shown because too many files have changed in this diff Show more