swift-sdk/Sources/MCP/Server/Server.swift
Mattt 1f5f8d64b9
Add configurable strict mode for MCP server initialization (#3)
Adds a Configuration struct to control server behavior around client initialization.
When strict mode is enabled, server enforces MCP spec requiring clients to initialize
before sending requests. Default mode is more permissive for non-compliant clients.
2025-03-04 06:02:43 -08:00

420 lines
15 KiB
Swift

import Logging
import SystemPackage
import struct Foundation.Data
import struct Foundation.Date
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
/// Model Context Protocol server
public actor Server {
/// The server configuration
public struct Configuration: Hashable, Codable, Sendable {
/// The default configuration.
public static let `default` = Configuration(strict: false)
/// The strict configuration.
public static let strict = Configuration(strict: true)
/// When strict mode is enabled, the server:
/// - Requires clients to send an initialize request before any other requests
/// - Rejects all requests from uninitialized clients with a protocol error
///
/// While the MCP specification requires clients to initialize the connection
/// before sending other requests, some implementations may not follow this.
/// Disabling strict mode allows the server to be more lenient with non-compliant
/// clients, though this may lead to undefined behavior.
public var strict: Bool
}
/// Implementation information
public struct Info: Hashable, Codable, Sendable {
/// The server name
public let name: String
/// The server version
public let version: String
public init(name: String, version: String) {
self.name = name
self.version = version
}
}
/// Server capabilities
public struct Capabilities: Hashable, Codable, Sendable {
/// Resources capabilities
public struct Resources: Hashable, Codable, Sendable {
/// Whether the list of resources has changed
public var list: Bool?
/// Whether the resource can be read
public var read: Bool?
/// Whether the resource can be subscribed to
public var subscribe: Bool?
/// Whether the list of resources has changed
public var listChanged: Bool?
public init(
list: Bool? = nil,
read: Bool? = nil,
subscribe: Bool? = nil,
listChanged: Bool? = nil
) {
self.list = list
self.read = read
self.subscribe = subscribe
self.listChanged = listChanged
}
}
/// Tools capabilities
public struct Tools: Hashable, Codable, Sendable {
/// Whether the server notifies clients when tools change
public var listChanged: Bool?
public init(listChanged: Bool? = nil) {
self.listChanged = listChanged
}
}
/// Prompts capabilities
public struct Prompts: Hashable, Codable, Sendable {
/// Whether the server notifies clients when prompts change
public var listChanged: Bool?
public init(listChanged: Bool? = nil) {
self.listChanged = listChanged
}
}
/// Logging capabilities
public struct Logging: Hashable, Codable, Sendable {
public init() {}
}
/// Logging capabilities
public var logging: Logging?
/// Prompts capabilities
public var prompts: Prompts?
/// Resources capabilities
public var resources: Resources?
/// Tools capabilities
public var tools: Tools?
public init(
logging: Logging? = nil,
prompts: Prompts? = nil,
resources: Resources? = nil,
tools: Tools? = nil
) {
self.logging = logging
self.prompts = prompts
self.resources = resources
self.tools = tools
}
}
/// Server information
private let serverInfo: Server.Info
/// The server connection
private var connection: (any Transport)?
/// The server logger
private var logger: Logger? {
get async {
await connection?.logger
}
}
/// The server name
public nonisolated var name: String { serverInfo.name }
/// The server version
public nonisolated var version: String { serverInfo.version }
/// The server capabilities
public var capabilities: Capabilities
/// The server configuration
public var configuration: Configuration
/// Request handlers
private var methodHandlers: [String: RequestHandlerBox] = [:]
/// Notification handlers
private var notificationHandlers: [String: [NotificationHandlerBox]] = [:]
/// Whether the server is initialized
private var isInitialized = false
/// The client information
private var clientInfo: Client.Info?
/// The client capabilities
private var clientCapabilities: Client.Capabilities?
/// The protocol version
private var protocolVersion: String?
/// The list of subscriptions
private var subscriptions: [String: Set<ID>] = [:]
/// The task for the message handling loop
private var task: Task<Void, Never>?
public init(
name: String,
version: String,
capabilities: Server.Capabilities = .init(),
configuration: Configuration = .default
) {
self.serverInfo = Server.Info(name: name, version: version)
self.capabilities = capabilities
self.configuration = configuration
}
/// Start the server
public func start(transport: any Transport) async throws {
self.connection = transport
registerDefaultHandlers()
try await transport.connect()
await logger?.info(
"Server started", metadata: ["name": "\(name)", "version": "\(version)"])
// Start message handling loop
task = Task {
do {
let stream = await transport.receive()
for try await string in stream {
if Task.isCancelled { break } // Check cancellation inside loop
var requestID: ID?
do {
guard let data = string.data(using: .utf8) else {
throw Error.parseError("Invalid UTF-8 data")
}
// Attempt to decode string data as AnyRequest or AnyMessage
let decoder = JSONDecoder()
if let request = try? decoder.decode(AnyRequest.self, from: data) {
try await handleRequest(request)
} else if let message = try? decoder.decode(AnyMessage.self, from: data) {
try await handleMessage(message)
} else {
// Try to extract request ID from raw JSON if possible
if let json = try? JSONDecoder().decode(
[String: Value].self, from: data),
let idValue = json["id"]
{
if let strValue = idValue.stringValue {
requestID = .string(strValue)
} else if let intValue = idValue.intValue {
requestID = .number(intValue)
}
}
throw Error.parseError("Invalid message format")
}
} catch let error as Errno where error == .resourceTemporarilyUnavailable {
// Resource temporarily unavailable, retry after a short delay
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
continue
} catch {
await logger?.error(
"Error processing message", metadata: ["error": "\(error)"])
// Only send error responses for requests
if let requestID = requestID {
let mcpError =
error as? Error ?? Error.internalError(error.localizedDescription)
let response = AnyMethod.response(id: requestID, error: mcpError)
try? await send(response)
}
}
}
} catch {
await logger?.error(
"Fatal error in message handling loop", metadata: ["error": "\(error)"])
}
}
}
/// Stop the server
public func stop() async {
task?.cancel()
task = nil
if let connection = connection {
await connection.disconnect()
}
connection = nil
}
// MARK: - Registration
/// Register a method handler
@discardableResult
public func withMethodHandler<M: Method>(
_ type: M.Type,
handler: @escaping @Sendable (M.Parameters) async throws -> M.Result
) -> Self {
methodHandlers[M.name] = TypedRequestHandler { (request: Request<M>) -> Response<M> in
let result = try await handler(request.params)
return Response(id: request.id, result: result)
}
return self
}
/// Register a notification handler
@discardableResult
public func onNotification<N: Notification>(
_ type: N.Type,
handler: @escaping @Sendable (Message<N>) async throws -> Void
) -> Self {
let handlers = notificationHandlers[N.name, default: []]
notificationHandlers[N.name] = handlers + [TypedNotificationHandler(handler)]
return self
}
// MARK: - Sending
/// Send a response to a client
public func send<M: Method>(_ response: Response<M>) async throws {
guard let connection = connection else {
throw Error.internalError("Server connection not initialized")
}
let responseData = try JSONEncoder().encode(response)
if let responseStr = String(data: responseData, encoding: .utf8) {
try await connection.send(responseStr)
}
}
/// Send a notification to connected clients
public func notify<N: Notification>(_ notification: Message<N>) async throws {
guard let connection = connection else {
throw Error.internalError("Server connection not initialized")
}
let notificationData = try JSONEncoder().encode(notification)
if let notificationStr = String(data: notificationData, encoding: .utf8) {
try await connection.send(notificationStr)
}
}
// MARK: -
private func handleRequest(_ request: Request<AnyMethod>) async throws {
await logger?.debug(
"Processing request",
metadata: [
"method": "\(request.method)",
"id": "\(request.id)",
])
if configuration.strict {
// The client SHOULD NOT send requests other than pings
// before the server has responded to the initialize request.
switch request.method {
case Initialize.name, Ping.name:
break
default:
try checkInitialized()
}
}
// Find handler for method name
guard let handler = methodHandlers[request.method] else {
let error = Error.methodNotFound("Unknown method: \(request.method)")
let response = AnyMethod.response(id: request.id, error: error)
try await send(response)
throw error
}
do {
// Handle request and get response
let response = try await handler(request)
try await send(response)
} catch {
let mcpError = error as? Error ?? Error.internalError(error.localizedDescription)
let response = AnyMethod.response(id: request.id, error: mcpError)
try await send(response)
throw error
}
}
private func handleMessage(_ message: Message<AnyNotification>) async throws {
await logger?.debug(
"Processing notification",
metadata: ["method": "\(message.method)"])
if configuration.strict {
// Check initialization state unless this is an initialized notification
if message.method != InitializedNotification.name {
try checkInitialized()
}
}
// 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)",
])
}
}
}
private func checkInitialized() throws {
guard isInitialized else {
throw Error.invalidRequest("Server is not initialized")
}
}
private func registerDefaultHandlers() {
// Initialize
withMethodHandler(Initialize.self) { [weak self] params in
guard let self = self else {
throw Error.internalError("Server was deallocated")
}
guard await !self.isInitialized else {
throw Error.invalidRequest("Server is already initialized")
}
// Validate protocol version
guard Version.latest == params.protocolVersion else {
throw Error.invalidRequest(
"Unsupported protocol version: \(params.protocolVersion)")
}
await self.setInitialState(
clientInfo: params.clientInfo,
clientCapabilities: params.capabilities,
protocolVersion: params.protocolVersion
)
let result = Initialize.Result(
protocolVersion: Version.latest,
capabilities: await self.capabilities,
serverInfo: self.serverInfo,
instructions: nil
)
// Send initialized notification after a short delay
Task {
try? await Task.sleep(nanoseconds: 100_000_000) // 100ms
try? await self.notify(InitializedNotification.message())
}
return result
}
// Ping
withMethodHandler(Ping.self) { _ in return Empty() }
}
private func setInitialState(
clientInfo: Client.Info,
clientCapabilities: Client.Capabilities,
protocolVersion: String
) async {
self.clientInfo = clientInfo
self.clientCapabilities = clientCapabilities
self.protocolVersion = protocolVersion
self.isInitialized = true
}
}