Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,89 @@ for message in messages {
}
```

### Sampling

Sampling allows servers to request LLM completions through the client,
enabling agentic behaviors while maintaining human-in-the-loop control.
Clients register a handler to process incoming sampling requests from servers.

> [!TIP]
> Sampling requests flow from **server to client**,
> not client to server.
> This enables servers to request AI assistance
> while clients maintain control over model access and user approval.

```swift
// Register a sampling handler in the client
await client.withSamplingHandler { parameters in
// Review the sampling request (human-in-the-loop step 1)
print("Server requests completion for: \(parameters.messages)")

// Optionally modify the request based on user input
var messages = parameters.messages
if let systemPrompt = parameters.systemPrompt {
print("System prompt: \(systemPrompt)")
}

// Sample from your LLM (this is where you'd call your AI service)
let completion = try await callYourLLMService(
messages: messages,
maxTokens: parameters.maxTokens,
temperature: parameters.temperature
)

// Review the completion (human-in-the-loop step 2)
print("LLM generated: \(completion)")
// User can approve, modify, or reject the completion here

// Return the result to the server
return CreateSamplingMessage.Result(
model: "your-model-name",
stopReason: .endTurn,
role: .assistant,
content: .text(completion)
)
}
```

The sampling flow follows these steps:

```mermaid
sequenceDiagram
participant S as MCP Server
participant C as MCP Client
participant U as User/Human
participant L as LLM Service

Note over S,L: Server-initiated sampling request
S->>C: sampling/createMessage request
Note right of S: Server needs AI assistance<br/>for decision or content

Note over C,U: Human-in-the-loop review #1
C->>U: Show sampling request
U->>U: Review & optionally modify<br/>messages, system prompt
U->>C: Approve request

Note over C,L: Client handles LLM interaction
C->>L: Send messages to LLM
L->>C: Return completion

Note over C,U: Human-in-the-loop review #2
C->>U: Show LLM completion
U->>U: Review & optionally modify<br/>or reject completion
U->>C: Approve completion

Note over C,S: Return result to server
C->>S: sampling/createMessage response
Note left of C: Contains model used,<br/>stop reason, final content

Note over S: Server continues with<br/>AI-assisted result
```

This human-in-the-loop design ensures that users
maintain control over what the LLM sees and generates,
even when servers initiate the requests.

### Error Handling

Handle common client errors:
Expand Down Expand Up @@ -504,6 +587,49 @@ server.withMethodHandler(GetPrompt.self) { params in
}
```

### Sampling

Servers can request LLM completions from clients through sampling. This enables agentic behaviors where servers can ask for AI assistance while maintaining human oversight.

> [!NOTE]
> The current implementation provides the correct API design for sampling, but requires bidirectional communication support in the transport layer. This feature will be fully functional when bidirectional transport support is added.

```swift
// Enable sampling capability in server
let server = Server(
name: "MyModelServer",
version: "1.0.0",
capabilities: .init(
sampling: .init(), // Enable sampling capability
tools: .init(listChanged: true)
)
)

// Request sampling from the client (conceptual - requires bidirectional transport)
do {
let result = try await server.requestSampling(
messages: [
Sampling.Message(role: .user, content: .text("Analyze this data and suggest next steps"))
],
systemPrompt: "You are a helpful data analyst",
maxTokens: 150,
temperature: 0.7
)

// Use the LLM completion in your server logic
print("LLM suggested: \(result.content)")

} catch {
print("Sampling request failed: \(error)")
}
```

Sampling enables powerful agentic workflows:
- **Decision-making**: Ask the LLM to choose between options
- **Content generation**: Request drafts for user approval
- **Data analysis**: Get AI insights on complex data
- **Multi-step reasoning**: Chain AI completions with tool calls

#### Initialize Hook

Control client connections with an initialize hook:
Expand Down
126 changes: 126 additions & 0 deletions Sources/MCP/Base/UnitInterval.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/// A value constrained to the range 0.0 to 1.0, inclusive.
///
/// `UnitInterval` represents a normalized value that is guaranteed to be within
/// the unit interval [0, 1]. This type is commonly used for representing
/// priorities in sampling request model preferences.
///
/// The type provides safe initialization that returns `nil` for values outside
/// the valid range, ensuring that all instances contain valid unit interval values.
///
/// - Example:
/// ```swift
/// let zero: UnitInterval = 0 // 0.0
/// let half = UnitInterval(0.5)! // 0.5
/// let one: UnitInterval = 1.0 // 1.0
/// let invalid = UnitInterval(1.5) // nil
/// ```
public struct UnitInterval: Hashable, Sendable {
private let value: Double

/// Creates a unit interval value from a `Double`.
///
/// - Parameter value: A double value that must be in the range 0.0...1.0
/// - Returns: A `UnitInterval` instance if the value is valid, `nil` otherwise
///
/// - Example:
/// ```swift
/// let valid = UnitInterval(0.75) // Optional(0.75)
/// let invalid = UnitInterval(-0.1) // nil
/// let boundary = UnitInterval(1.0) // Optional(1.0)
/// ```
public init?(_ value: Double) {
guard (0...1).contains(value) else { return nil }
self.value = value
}

/// The underlying double value.
///
/// This property provides access to the raw double value that is guaranteed
/// to be within the range [0, 1].
///
/// - Returns: The double value between 0.0 and 1.0, inclusive
public var doubleValue: Double { value }
}

// MARK: - Comparable

extension UnitInterval: Comparable {
public static func < (lhs: UnitInterval, rhs: UnitInterval) -> Bool {
lhs.value < rhs.value
}
}

// MARK: - CustomStringConvertible

extension UnitInterval: CustomStringConvertible {
public var description: String { "\(value)" }
}

// MARK: - Codable

extension UnitInterval: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let doubleValue = try container.decode(Double.self)
guard let interval = UnitInterval(doubleValue) else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Value \(doubleValue) is not in range 0...1")
)
}
self = interval
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value)
}
}

// MARK: - ExpressibleByFloatLiteral

extension UnitInterval: ExpressibleByFloatLiteral {
/// Creates a unit interval from a floating-point literal.
///
/// This initializer allows you to create `UnitInterval` instances using
/// floating-point literals. The literal value must be in the range [0, 1]
/// or a runtime error will occur.
///
/// - Parameter value: A floating-point literal between 0.0 and 1.0
///
/// - Warning: This initializer will crash if the literal is outside the valid range.
/// Use the failable initializer `init(_:)` for runtime validation.
///
/// - Example:
/// ```swift
/// let quarter: UnitInterval = 0.25
/// let half: UnitInterval = 0.5
/// ```
public init(floatLiteral value: Double) {
self.init(value)!
}
}

// MARK: - ExpressibleByIntegerLiteral

extension UnitInterval: ExpressibleByIntegerLiteral {
/// Creates a unit interval from an integer literal.
///
/// This initializer allows you to create `UnitInterval` instances using
/// integer literals. Only the values 0 and 1 are valid.
///
/// - Parameter value: An integer literal, either 0 or 1
///
/// - Warning: This initializer will crash if the literal is outside the valid range.
/// Use the failable initializer `init(_:)` for runtime validation.
///
/// - Example:
/// ```swift
/// let zero: UnitInterval = 0
/// let one: UnitInterval = 1
/// ```
public init(integerLiteral value: Int) {
self.init(Double(value))!
}
}
39 changes: 39 additions & 0 deletions Sources/MCP/Client/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,45 @@ public actor Client {
return (content: result.content, isError: result.isError)
}

// MARK: - Sampling

/// Register a handler for sampling requests from servers
///
/// Sampling allows servers to request LLM completions through the client,
/// enabling sophisticated agentic behaviors while maintaining human-in-the-loop control.
///
/// The sampling flow follows these steps:
/// 1. Server sends a `sampling/createMessage` request to the client
/// 2. Client reviews the request and can modify it (via this handler)
/// 3. Client samples from an LLM (via this handler)
/// 4. Client reviews the completion (via this handler)
/// 5. Client returns the result to the server
///
/// - Parameter handler: A closure that processes sampling requests and returns completions
/// - Returns: Self for method chaining
/// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/sampling#how-sampling-works
@discardableResult
public func withSamplingHandler(
_ handler: @escaping @Sendable (CreateSamplingMessage.Parameters) async throws ->
CreateSamplingMessage.Result
) -> Self {
// Note: This would require extending the client architecture to handle incoming requests from servers.
// The current MCP Swift SDK architecture assumes clients only send requests to servers,
// but sampling requires bidirectional communication where servers can send requests to clients.
//
// A full implementation would need:
// 1. Request handlers in the client (similar to how servers handle requests)
// 2. Bidirectional transport support
// 3. Request/response correlation for server-to-client requests
//
// For now, this serves as the correct API design for when bidirectional support is added.

// This would register the handler similar to how servers register method handlers:
// methodHandlers[CreateSamplingMessage.name] = TypedRequestHandler(handler)

return self
}

// MARK: -

private func handleResponse(_ response: Response<AnyMethod>) async {
Expand Down
Loading