Skip to content

Commit 431ce36

Browse files
committed
Encode dates in ISO 8601, decode a string for a Date argument as ISO 8601 or unix time stamp
1 parent c17161f commit 431ce36

File tree

7 files changed

+130
-5
lines changed

7 files changed

+130
-5
lines changed

Demos/SwiftMCPDemo/Calculator.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,30 @@ actor Calculator {
3838
}
3939
}
4040

41+
/**
42+
Gets the current date/time on the server
43+
- Returns: The current time
44+
*/
45+
@MCPTool
46+
func getCurrentDateTime() -> Date {
47+
return Date()
48+
}
49+
50+
/**
51+
Formats a date/time as String
52+
- Parameter date: The Date to format
53+
- Returns: A string with the date formatted
54+
*/
55+
@MCPTool
56+
func formatDateAsString(date: Date) -> String {
57+
58+
let dateFormatter = DateFormatter()
59+
dateFormatter.dateStyle = .long
60+
dateFormatter.timeStyle = .long
61+
62+
return dateFormatter.string(from: date)
63+
}
64+
4165
/// Adds two integers and returns their sum
4266
/// - Parameter a: First number to add
4367
/// - Parameter b: Second number to add

Sources/SwiftMCP/Extensions/Dictionary+ParameterExtraction.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ public extension Dictionary where Key == String, Value == Sendable {
4545
// return the actual enum case value that matches the string label
4646
return allCases[index]
4747
}
48+
else if T.self == Date.self {
49+
// Handle Date type using the new extractDate method
50+
let date = try extractDate(named: name)
51+
return date as! T
52+
}
4853
else {
4954
throw MCPToolError.invalidArgumentType(
5055
parameterName: name,
@@ -165,4 +170,48 @@ public extension Dictionary where Key == String, Value == Sendable {
165170
)
166171
}
167172
}
173+
174+
/// Extracts a Date parameter from the dictionary, attempting multiple parsing strategies
175+
/// - Parameter name: The name of the parameter
176+
/// - Returns: The extracted Date value
177+
/// - Throws: MCPToolError.invalidArgumentType if the parameter cannot be converted to a Date
178+
func extractDate(named name: String) throws -> Date {
179+
guard let anyValue = self[name] else {
180+
preconditionFailure("Failed to retrieve value for parameter \(name)")
181+
}
182+
183+
// If it's already a Date, return it
184+
if let date = anyValue as? Date {
185+
return date
186+
}
187+
188+
// Try parsing as string
189+
if let stringValue = anyValue as? String {
190+
// Try ISO 8601 date format
191+
let isoFormatter = ISO8601DateFormatter()
192+
if let date = isoFormatter.date(from: stringValue) {
193+
return date
194+
}
195+
196+
// Try Unix timestamp (both integer and decimal)
197+
if let timestampDouble = Double(stringValue) {
198+
return Date(timeIntervalSince1970: timestampDouble)
199+
}
200+
}
201+
202+
// Try direct conversion from number
203+
if let timestampDouble = anyValue as? Double {
204+
return Date(timeIntervalSince1970: timestampDouble)
205+
}
206+
207+
if let timestampInt = anyValue as? Int {
208+
return Date(timeIntervalSince1970: TimeInterval(timestampInt))
209+
}
210+
211+
throw MCPToolError.invalidArgumentType(
212+
parameterName: name,
213+
expectedType: "ISO 8601 Date",
214+
actualType: String(describing: Swift.type(of: anyValue))
215+
)
216+
}
168217
}

Sources/SwiftMCP/Protocols/MCPServer.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,9 @@ public extension MCPServer {
252252
}
253253
else
254254
{
255-
let jsonData = try JSONEncoder().encode(result)
255+
let encoder = JSONEncoder()
256+
encoder.dateEncodingStrategy = .iso8601
257+
let jsonData = try encoder.encode(result)
256258
responseText = String(data: jsonData, encoding: .utf8) ?? ""
257259
}
258260

Sources/SwiftMCP/Transport/HTTPHandler.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,9 @@ final class HTTPHandler: ChannelInboundHandler, Identifiable, @unchecked Sendabl
245245
return
246246
}
247247

248-
let decoder = JSONDecoder()
249248
do {
249+
let decoder = JSONDecoder()
250+
decoder.dateDecodingStrategy = .iso8601
250251
let request = try decoder.decode(JSONRPCMessage.self, from: body)
251252

252253
if request.method == nil {

Sources/SwiftMCP/Transport/StdioTransport.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ public final class StdioTransport: Transport, @unchecked Sendable {
6565

6666
logger.trace("Received input: \(input)")
6767

68-
let request = try JSONDecoder().decode(JSONRPCMessage.self, from: data)
68+
let decoder = JSONDecoder()
69+
decoder.dateDecodingStrategy = .iso8601
70+
let request = try decoder.decode(JSONRPCMessage.self, from: data)
6971

7072
// Handle the request.
7173
if let response = await server.handleRequest(request) {
@@ -107,7 +109,9 @@ public final class StdioTransport: Transport, @unchecked Sendable {
107109

108110
logger.trace("Received input: \(input)")
109111

110-
let request = try JSONDecoder().decode(JSONRPCMessage.self, from: data)
112+
let decoder = JSONDecoder()
113+
decoder.dateDecodingStrategy = .iso8601
114+
let request = try decoder.decode(JSONRPCMessage.self, from: data)
111115

112116
// Handle the request.
113117
if let response = await server.handleRequest(request) {

Tests/SwiftMCPTests/Calculator.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,13 @@ class Calculator {
8484
func noop() {
8585

8686
}
87+
88+
/**
89+
Gets the current date/time on the server
90+
- Returns: The current time
91+
*/
92+
@MCPTool
93+
func getCurrentDateTime() -> Date {
94+
return Date()
95+
}
8796
}

Tests/SwiftMCPTests/CalculatorMockTests.swift

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,43 @@ func testNoopViaMockClient() async throws {
291291

292292
#expect(type == "text")
293293
#expect(text == "") // noop returns empty string
294-
}
294+
}
295+
296+
@Test("Tests getCurrentDateTime returns ISO formatted date")
297+
func testGetCurrentDateTimeViaMockClient() async throws {
298+
let calculator = Calculator()
299+
let client = MockClient(calculator: calculator)
300+
301+
let request = JSONRPCMessage(
302+
id: 9,
303+
method: "tools/call",
304+
params: [
305+
"name": "getCurrentDateTime",
306+
"arguments": [:]
307+
]
308+
)
309+
310+
let response = try await client.send(request)
311+
312+
#expect(response.id == 9)
313+
#expect(response.error == nil)
314+
315+
let result = unwrap(response.result)
316+
let isError = unwrap(result["isError"]?.value as? Bool)
317+
#expect(isError == false)
318+
319+
let content = unwrap(result["content"]?.value as? [[String: String]])
320+
let firstContent = unwrap(content.first)
321+
let type = unwrap(firstContent["type"])
322+
let text = unwrap(firstContent["text"])
323+
324+
#expect(type == "text")
325+
326+
// Verify the text is a valid ISO 8601 date string
327+
let dateFormatter = ISO8601DateFormatter()
328+
let date = dateFormatter.date(from: text.replacingOccurrences(of: "\"", with: ""))
329+
#expect(date != nil, "Response should be a valid ISO 8601 date string")
330+
}
295331

296332
/// Errors that can occur during MCP client operations
297333
public enum MCPError: LocalizedError {

0 commit comments

Comments
 (0)