feat: 2025-11-25 review fixes (#201)
* fix: review fixes of the latest changes * fix: updated unit tests * chore: more conformance compliance improvements * chore: reverted protocol changes * chore: more compliance improvements * chore: more conformance compliance improvements * chore: improvements to the backcompat and updated to Tool unit tests * chore: ci update
This commit is contained in:
parent
6112a3995a
commit
973b46b8f7
1 changed files with 692 additions and 507 deletions
|
|
@ -34,11 +34,27 @@ public actor Server {
|
|||
public let title: String?
|
||||
/// The server version
|
||||
public let version: String
|
||||
/// Optional description of the server
|
||||
public let description: String?
|
||||
/// Optional website URL for the server
|
||||
public let websiteUrl: String?
|
||||
/// Optional set of sized icons for display in a user interface
|
||||
public let icons: [Icon]?
|
||||
|
||||
public init(name: String, version: String, title: String? = nil) {
|
||||
public init(
|
||||
name: String,
|
||||
version: String,
|
||||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
websiteUrl: String? = nil,
|
||||
icons: [Icon]? = nil
|
||||
) {
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.version = version
|
||||
self.description = description
|
||||
self.websiteUrl = websiteUrl
|
||||
self.icons = icons
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,11 +101,6 @@ public actor Server {
|
|||
public init() {}
|
||||
}
|
||||
|
||||
/// Sampling capabilities
|
||||
public struct Sampling: Hashable, Codable, Sendable {
|
||||
public init() {}
|
||||
}
|
||||
|
||||
/// Completions capabilities
|
||||
public struct Completions: Hashable, Codable, Sendable {
|
||||
public init() {}
|
||||
|
|
@ -103,8 +114,6 @@ public actor Server {
|
|||
public var prompts: Prompts?
|
||||
/// Resources capabilities
|
||||
public var resources: Resources?
|
||||
/// Sampling capabilities
|
||||
public var sampling: Sampling?
|
||||
/// Tools capabilities
|
||||
public var tools: Tools?
|
||||
|
||||
|
|
@ -113,14 +122,12 @@ public actor Server {
|
|||
logging: Logging? = nil,
|
||||
prompts: Prompts? = nil,
|
||||
resources: Resources? = nil,
|
||||
sampling: Sampling? = nil,
|
||||
tools: Tools? = nil
|
||||
) {
|
||||
self.completions = completions
|
||||
self.logging = logging
|
||||
self.prompts = prompts
|
||||
self.resources = resources
|
||||
self.sampling = sampling
|
||||
self.tools = tools
|
||||
}
|
||||
}
|
||||
|
|
@ -161,47 +168,6 @@ public actor Server {
|
|||
/// Pending request tasks (for cancellation support)
|
||||
private var pendingRequestTasks: [ID: Task<Response<AnyMethod>, Error>] = [:]
|
||||
|
||||
/// An error indicating a type mismatch when decoding a pending request
|
||||
private struct TypeMismatchError: Swift.Error {}
|
||||
|
||||
/// A pending request with a continuation for the result
|
||||
private struct PendingRequest<T> {
|
||||
let continuation: CheckedContinuation<T, Swift.Error>
|
||||
}
|
||||
|
||||
/// A type-erased pending request
|
||||
private struct AnyPendingRequest: Sendable {
|
||||
private let _resume: @Sendable (Result<Any, Swift.Error>) -> Void
|
||||
|
||||
init<T: Sendable & Decodable>(_ request: PendingRequest<T>) {
|
||||
_resume = { result in
|
||||
switch result {
|
||||
case .success(let value):
|
||||
if let typedValue = value as? T {
|
||||
request.continuation.resume(returning: typedValue)
|
||||
} else if let value = value as? Value,
|
||||
let data = try? JSONEncoder().encode(value),
|
||||
let decoded = try? JSONDecoder().decode(T.self, from: data)
|
||||
{
|
||||
request.continuation.resume(returning: decoded)
|
||||
} else {
|
||||
request.continuation.resume(throwing: TypeMismatchError())
|
||||
}
|
||||
case .failure(let error):
|
||||
request.continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resume(returning value: Any) {
|
||||
_resume(.success(value))
|
||||
}
|
||||
|
||||
func resume(throwing error: Swift.Error) {
|
||||
_resume(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// Pending requests sent to the client, awaiting responses
|
||||
private var pendingRequests: [ID: AnyPendingRequest] = [:]
|
||||
|
||||
|
|
@ -330,6 +296,16 @@ public actor Server {
|
|||
await task?.value
|
||||
}
|
||||
|
||||
// MARK: - Request Context
|
||||
|
||||
/// The JSON-RPC request ID of the currently executing method handler.
|
||||
///
|
||||
/// Set via `@TaskLocal` before dispatching each request, so it propagates
|
||||
/// automatically into the handler task. Accessible package-wide for
|
||||
/// transports that need to identify the active request (e.g. closing an
|
||||
/// SSE stream mid-call for reconnection testing per SEP-1699).
|
||||
@TaskLocal package static var currentRequestID: ID? = nil
|
||||
|
||||
// MARK: - Registration
|
||||
|
||||
/// Register a method handler
|
||||
|
|
@ -523,8 +499,8 @@ public actor Server {
|
|||
/// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/elicitation
|
||||
public func requestElicitation(
|
||||
message: String,
|
||||
requestedSchema: Elicitation.RequestSchema,
|
||||
mode: Elicitation.Mode? = nil,
|
||||
requestedSchema: Elicitation.RequestSchema? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) async throws -> CreateElicitation.Result {
|
||||
guard connection != nil else {
|
||||
|
|
@ -774,26 +750,30 @@ public actor Server {
|
|||
return response
|
||||
}
|
||||
|
||||
// Create a task to handle the request with cancellation support
|
||||
let handlerTask = Task<Response<AnyMethod>, Error> {
|
||||
do {
|
||||
// Check if task was cancelled before starting
|
||||
try Task.checkCancellation()
|
||||
// Create a task to handle the request with cancellation support.
|
||||
// Set currentRequestID as a task local so handlers can identify the active request.
|
||||
var handlerTask: Task<Response<AnyMethod>, Error>!
|
||||
Server.$currentRequestID.withValue(request.id) {
|
||||
handlerTask = Task<Response<AnyMethod>, Error> {
|
||||
do {
|
||||
// Check if task was cancelled before starting
|
||||
try Task.checkCancellation()
|
||||
|
||||
// Handle request and get response
|
||||
let response = try await handler(request)
|
||||
return response
|
||||
} catch is CancellationError {
|
||||
// Request was cancelled, don't send a response per MCP spec
|
||||
await logger?.debug(
|
||||
"Request cancelled",
|
||||
metadata: ["id": "\(request.id)", "method": "\(request.method)"]
|
||||
)
|
||||
throw CancellationError()
|
||||
} catch {
|
||||
let mcpError =
|
||||
error as? MCPError ?? MCPError.internalError(error.localizedDescription)
|
||||
return AnyMethod.response(id: request.id, error: mcpError)
|
||||
// Handle request and get response
|
||||
let response = try await handler(request)
|
||||
return response
|
||||
} catch is CancellationError {
|
||||
// Request was cancelled, don't send a response per MCP spec
|
||||
await logger?.debug(
|
||||
"Request cancelled",
|
||||
metadata: ["id": "\(request.id)", "method": "\(request.method)"]
|
||||
)
|
||||
throw CancellationError()
|
||||
} catch {
|
||||
let mcpError =
|
||||
error as? MCPError ?? MCPError.internalError(error.localizedDescription)
|
||||
return AnyMethod.response(id: request.id, error: mcpError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -969,11 +949,19 @@ public actor Server {
|
|||
await self.logger?.debug(
|
||||
"Received cancellation notification",
|
||||
metadata: [
|
||||
"requestId": "\(requestId)",
|
||||
"requestId": requestId.map { "\($0)" } ?? "none",
|
||||
"reason": reason.map { "\($0)" } ?? "none",
|
||||
]
|
||||
)
|
||||
|
||||
guard let requestId = requestId else {
|
||||
await self.logger?.warning(
|
||||
"Received cancellation notification with no requestId (violates spec MUST)",
|
||||
metadata: ["reason": reason.map { "\($0)" } ?? "none"]
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel the pending request task if it exists and remove from tracking
|
||||
if let task = await self.removePendingRequest(id: requestId) {
|
||||
task.cancel()
|
||||
|
|
|
|||
Loading…
Reference in a new issue