diff --git a/Package.resolved b/Package.resolved index 5e9023c5..fb776dd5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,10 +1,10 @@ { - "originHash" : "08de61941b7919a65e36c0e34f8c1c41995469b86a39122158b75b4a68c4527d", + "originHash" : "371f3dfcfa1201fc8d50e924ad31f9ebc4f90242924df1275958ac79df15dc12", "pins" : [ { "identity" : "eventsource", "kind" : "remoteSourceControl", - "location" : "https://github.com/loopwork-ai/eventsource.git", + "location" : "https://github.com/mattt/eventsource.git", "state" : { "revision" : "e83f076811f32757305b8bf69ac92d05626ffdd7", "version" : "1.1.0" diff --git a/README.md b/README.md index ac23a4c3..93d6467c 100644 --- a/README.md +++ b/README.md @@ -395,6 +395,43 @@ do { > [!NOTE] > `Server` automatically handles batch requests from MCP clients. +#### Progress Notifications + +MCP supports optional progress tracking for long-running operations through notifications. Either side can send progress notifications to provide updates about operation status. + +For example, a client might wish to update its loading UI whenever the server sends a progress notification for a particular `CallTool` request: + +```swift +let callToolProgressToken = "abc-123" + +// Register notification handler before invoking callTool +await client.onNotification(ProgressNotification.self) { message in + // Access progress notifications for a particular request by filtering for that request's progressToken + guard message.params.progressToken == callToolProgressToken else { return } + + if let progressTotal = message.params.total { + let percentProgress = message.params.progress / progressTotal + // update loading UI with percent progress + } + + if let progressMessage = message.params.message { + // update loading UI with human-readable progress information + } +} + +// Call a tool with a progressToken specified in the _meta arguments +try await client.callTool( + name: "content-recommender", + arguments: [ + "prompt": "The cutest Samoyed accounts across all social media", + "_meta": [ + "progressToken": callToolProgressToken + ] + ] +) +``` + + ## Server Usage The server component allows your application to host model capabilities and respond to client requests. @@ -886,4 +923,4 @@ see the [GitHub Releases page](https://github.com/modelcontextprotocol/swift-sdk This project is licensed under the MIT License. [mcp]: https://modelcontextprotocol.io -[mcp-spec-2025-03-26]: https://modelcontextprotocol.io/specification/2025-03-26 \ No newline at end of file +[mcp-spec-2025-03-26]: https://modelcontextprotocol.io/specification/2025-03-26 diff --git a/Sources/MCP/Base/Utilities/Progress.swift b/Sources/MCP/Base/Utilities/Progress.swift new file mode 100644 index 00000000..a5549cc1 --- /dev/null +++ b/Sources/MCP/Base/Utilities/Progress.swift @@ -0,0 +1,28 @@ +/// The Model Context Protocol includes optional progress tracking that allows receive updates about the progress of a request. +/// - Important: When a party wants to receive progress updates for a request, it must (1) include a `progressToken` in that request's metadata and (2) subscribe to `ProgressNotification` via`onNotification`. See README.md for example usage. +/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress +public struct ProgressNotification: Notification { + public static let name: String = "notifications/progress" + + public struct Parameters: Hashable, Codable, Sendable { + /// The original progress token. + public let progressToken: String + + /// The current progress value so far. + public let progress: Double + + /// An optional “total” progress value. + public let total: Double? + + /// An optional “message” value. + public let message: String? + + public init(progressToken: String, progress: Double, total: Double?, message: String?) { + self.progressToken = progressToken + self.progress = progress + self.total = total + self.message = message + } + } +} + diff --git a/Tests/MCPTests/NotificationTests.swift b/Tests/MCPTests/NotificationTests.swift index 4bdd1873..c898cb79 100644 --- a/Tests/MCPTests/NotificationTests.swift +++ b/Tests/MCPTests/NotificationTests.swift @@ -113,6 +113,43 @@ struct NotificationTests { #expect(decoded.params.uri == "test://resource") } + @Test("Progress notification with parameters") + func testProgressNotification() throws { + let params = ProgressNotification.Parameters( + progressToken: "some-token", + progress: 20, + total: 25, + message: "beep boop bop" + ) + let notification = ProgressNotification.message(params) + + #expect(notification.method == ProgressNotification.name) + #expect(notification.params.progressToken == "some-token") + #expect(notification.params.progress == 20) + #expect(notification.params.total == 25) + #expect(notification.params.message == "beep boop bop") + + let encoder = JSONEncoder() + let decoder = JSONDecoder() + + let data = try encoder.encode(notification) + + // Verify the exact JSON structure + let json = try JSONDecoder().decode([String: Value].self, from: data) + #expect(json["jsonrpc"] == "2.0") + #expect(json["method"] == "notifications/progress") + #expect(json["params"] != nil) + #expect(json.count == 3, "Should contain jsonrpc, method, and params fields") + + // Verify we can decode it back + let decoded = try decoder.decode(Message.self, from: data) + #expect(decoded.method == ProgressNotification.name) + #expect(decoded.params.progressToken == "some-token") + #expect(decoded.params.progress == 20) + #expect(decoded.params.total == 25) + #expect(decoded.params.message == "beep boop bop") + } + @Test("AnyNotification decoding - without params") func testAnyNotificationDecodingWithoutParams() throws { // Test decoding when params field is missing diff --git a/Tests/MCPTests/ProgressTests.swift b/Tests/MCPTests/ProgressTests.swift new file mode 100644 index 00000000..e3537800 --- /dev/null +++ b/Tests/MCPTests/ProgressTests.swift @@ -0,0 +1,18 @@ +import Testing + +@testable import MCP + +@Test("ProgressNotification parameters validation") +func testProgressNotification() throws { + let params = ProgressNotification.Parameters( + progressToken: "some-token", + progress: 20, + total: 25, + message: "beep boop bop" + ) + #expect(params.progressToken == "some-token") + #expect(params.progress == 20) + #expect(params.total == 25) + #expect(params.message == "beep boop bop") + #expect(ProgressNotification.name == "notifications/progress") +}