Created
April 1, 2025 17:25
-
-
Save sebsto/9cdc1bfec3ab905c8cb037b167373f5f to your computer and use it in GitHub Desktop.
MCP swift-SDK wrapper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import MCP | |
#if canImport(FoundationEssentials) | |
import FoundatioNEssentials | |
#else | |
import Foundation | |
#endif | |
public protocol MCPTool<Input, Output>: Sendable { | |
associatedtype Input | |
associatedtype Output | |
var name: String { get } | |
var description: String { get } | |
// FIXME: we need a way to generate this from the actual handler type :-) | |
var inputSchema: String { get } | |
// a generic handler | |
func handler(_ input: Input) async throws -> Output | |
} | |
public struct ClosureMCPTool<Input: Decodable, Output>: MCPTool { | |
public let name: String | |
public let description: String | |
public let inputSchema: String | |
let body: @Sendable (Input) async throws -> Output | |
init( | |
name: String, description: String, | |
inputSchema: String, | |
body: @Sendable @escaping (Input) async throws -> Output | |
) | |
where Output: Encodable { | |
self.name = name | |
self.description = description | |
self.inputSchema = inputSchema | |
self.body = body | |
} | |
public func handler(_ input: Input) async throws -> Output { | |
return try await self.body(input) | |
} | |
} | |
public enum Transport { | |
case stdio(StdioTransport) | |
// case http(HttpTransport) | |
} | |
//TODO: add a logger into the game | |
public func withMCPServer<Input: Decodable, Output>( | |
name: String, | |
version: String, | |
tools: [any MCPTool<Input, Output>], | |
transport: Transport = .stdio(StdioTransport()), | |
_ converter: @Sendable @escaping (CallTool.Parameters) async throws -> Input | |
) async throws { | |
// create the server | |
let server = Server( | |
name: name, | |
version: version, | |
capabilities: .init( | |
tools: .init() | |
) | |
) | |
// register the tools, part 1 : tools/list | |
await server.withMethodHandler(ListTools.self) { params in | |
let _tools = try tools.map { tool in | |
Tool( | |
name: tool.name, | |
description: tool.description, | |
inputSchema: try JSONDecoder().decode( | |
Value.self, from: tool.inputSchema.data(using: .utf8)!) | |
) | |
} | |
return ListTools.Result(tools: _tools, nextCursor: nil) | |
} | |
// register the tools, part 2 : tools/call | |
await server.withMethodHandler(CallTool.self) { params in | |
// Check if the tool name is in our list of tools | |
guard let tool = tools.first(where: { $0.name == params.name }) else { | |
throw MCPError.unknownTool(params.name) | |
} | |
// let the caller convert the parameters to the expected input type | |
//FIXME: we need to allow user to provide one converter function for each tool | |
let input = try await converter(params) | |
// call the tool | |
let output = try await tool.handler(input) | |
// return the result | |
return CallTool.Result(content: [.text(String(describing: output))]) | |
} | |
// start the server with the given transport | |
switch transport { | |
case .stdio(let t): | |
try await server.start(transport: t) | |
} | |
await server.waitUntilCompleted() | |
} | |
enum MCPError: Swift.Error, LocalizedError { | |
case missingparam(String) | |
case invalidParam(String, String) | |
case unknownTool(String) | |
public var errorDescription: String? { | |
switch self { | |
case .missingparam(let name): | |
return "Missing parameter \(name)" | |
case .invalidParam(let name, let value): | |
return "Invalid parameter \(name) with value \(value)" | |
case .unknownTool(let name): | |
return "Unknown tool \(name)" | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment