Skip to content
Open
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
21 changes: 19 additions & 2 deletions Sources/MCP/Base/ID.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import struct Foundation.UUID
import struct os.OSAllocatedUnfairLock

/// A unique identifier for a request.
public enum ID: Hashable, Sendable {
Expand All @@ -8,9 +9,25 @@ public enum ID: Hashable, Sendable {
/// A number ID.
case number(Int)

/// Generates a random string ID.
/// Process-wide counter for monotonic numeric request IDs.
///
/// Integer IDs are JSON-RPC 2.0–valid and avoid silent hangs against servers
/// that only accept numeric `id` values (e.g. Apple's `mcpbridge`).
private static let counter = OSAllocatedUnfairLock(initialState: 0)

/// Generates a monotonic numeric ID suitable for outbound client requests.
public static var random: ID {
return .string(UUID().uuidString)
.number(
counter.withLock { value in
value += 1
return value
}
)
}

/// Generates a random UUID string ID (previous `random` behavior).
public static var randomUUID: ID {
.string(UUID().uuidString)
}
}

Expand Down
23 changes: 20 additions & 3 deletions Tests/MCPTests/IDTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,35 @@ struct IDTests {
#expect(decoded == id)
}

@Test("Random ID generation")
@Test("Random ID generation uses monotonic integers")
func testRandomID() throws {
let id1 = ID.random
let id2 = ID.random
#expect(id1 != id2, "Random IDs should be unique")

guard case .number(let n1) = id1, case .number(let n2) = id2 else {
Issue.record("Random ID should be number type")
return
}
#expect(n2 > n1)

let encoder = JSONEncoder()
let data = try encoder.encode(id1)
let json = String(data: data, encoding: .utf8)
#expect(json == String(n1), "Encoded random ID should be a bare JSON number")
}

@Test("Random UUID ID generation")
func testRandomUUIDID() throws {
let id1 = ID.randomUUID
let id2 = ID.randomUUID
#expect(id1 != id2, "UUID IDs should be unique")

if case .string(let str) = id1 {
#expect(!str.isEmpty)
// Verify it's a valid UUID string
#expect(UUID(uuidString: str) != nil)
} else {
#expect(Bool(false), "Random ID should be string type")
Issue.record("randomUUID ID should be string type")
}
}
}