Skip to content

Instantly share code, notes, and snippets.

@sebsto
Created April 1, 2025 17:25
Show Gist options
  • Save sebsto/9cdc1bfec3ab905c8cb037b167373f5f to your computer and use it in GitHub Desktop.
Save sebsto/9cdc1bfec3ab905c8cb037b167373f5f to your computer and use it in GitHub Desktop.
MCP swift-SDK wrapper
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