Skip to content

Commit 2134f24

Browse files
committed
Merge branch 'enum'
2 parents 5ab9880 + 109e420 commit 2134f24

File tree

15 files changed

+222
-112
lines changed

15 files changed

+222
-112
lines changed

Demos/SwiftMCPDemo/Calculator.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import Foundation
22
import SwiftMCP
33

4+
enum Options: CaseIterable {
5+
case all
6+
case unread
7+
}
8+
49
/**
510
A Calculator for simple math doing additionals, subtractions etc.
611
*/
@@ -11,9 +16,15 @@ actor Calculator {
1116
/// - Parameter subject: The subject of the email
1217
/// - Returns Some confirmation
1318
@MCPTool
14-
func searchEmailSubject(for subject: String) async throws -> String
19+
func searchEmailSubject(for subject: String, option: Options) async throws -> String
1520
{
16-
return "Subject is \(subject)"
21+
switch option
22+
{
23+
case .all:
24+
return "Search in all emails, for subject \(subject)"
25+
case .unread:
26+
return "Search in unread emails, for subject \(subject)"
27+
}
1728
}
1829

1930
/// Adds two integers and returns their sum
@@ -40,7 +51,7 @@ actor Calculator {
4051
- Returns: A string representation of the array
4152
*/
4253
@MCPTool(description: "Custom description: Tests array processing")
43-
func testArray(a: [Int]) -> String {
54+
func testArray(a: [Int] = [1,2,3]) -> String {
4455
return a.map(String.init).joined(separator: ", ")
4556
}
4657

Demos/SwiftMCPDemo/Commands/HTTPSSECommand.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ final class HTTPSSECommand: AsyncParsableCommand {
9696

9797
let calculator = Calculator()
9898

99+
print(calculator.mcpToolMetadata)
100+
print(calculator.mcpTools)
101+
99102
let host = String.localHostname
100103
print("MCP Server \(calculator.serverName) (\(calculator.serverVersion)) started with HTTP+SSE transport on http://\(host):\(port)/sse")
101104

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// Array+CaseLabels.swift
3+
// SwiftMCP
4+
//
5+
// Created by Oliver Drobnik on 26.03.25.
6+
//
7+
8+
import Foundation
9+
10+
/**
11+
An extension on Array that provides functionality for extracting case labels from CaseIterable types.
12+
13+
This extension allows creating an array of strings from the case labels of any type that conforms to CaseIterable.
14+
The case labels are extracted using the type's string representation, with special handling for cases with associated values.
15+
16+
If the enum conforms to CustomStringConvertible, the case labels will be determined by the custom description implementation.
17+
This allows for customization of how enum cases are represented in MCP tools.
18+
*/
19+
extension Array where Element == String {
20+
/**
21+
Initialize an array of case labels if the given parameter (a type) conforms to CaseIterable.
22+
23+
- Parameters:
24+
- type: The type to extract case labels from. Must conform to CaseIterable.
25+
26+
- Returns: An array of strings containing the case labels, or nil if the type doesn't conform to CaseIterable.
27+
28+
- Note: For cases with associated values, this initializer will extract the case name without the associated values.
29+
For example, for a case like `case example(value: Int)`, it will return `"example"`.
30+
31+
- Note: If the enum conforms to CustomStringConvertible, the case labels will be determined by the custom description implementation.
32+
This allows for customization of how enum cases are represented in MCP tools.
33+
*/
34+
public init?<T>(caseLabelsFrom type: T.Type) {
35+
// Check if T conforms to CaseIterable at runtime.
36+
guard let caseIterableType = type as? any CaseIterable.Type else {
37+
return nil
38+
}
39+
40+
let cases = caseIterableType.allCases
41+
self = cases.map { caseValue in
42+
let description = String(describing: caseValue)
43+
44+
// trim off associated value if any
45+
if let parenIndex = description.firstIndex(of: "(") {
46+
return String(description[..<parenIndex])
47+
}
48+
49+
return description
50+
}
51+
}
52+
}
53+
54+
extension CaseIterable
55+
{
56+
static var caseLabels: [String] {
57+
return self.allCases.map { caseValue in
58+
let description = String(describing: caseValue)
59+
60+
// trim off associated value if any
61+
if let parenIndex = description.firstIndex(of: "(") {
62+
return String(description[..<parenIndex])
63+
}
64+
65+
return description
66+
}
67+
}
68+
}

Sources/SwiftMCP/Extensions/Dictionary+ParameterExtraction.swift

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,48 @@ public extension Dictionary where Key == String, Value == Sendable {
88
/// - Returns: The extracted value of type T
99
/// - Throws: MCPToolError.invalidArgumentType if the parameter cannot be converted to type T
1010
func extractParameter<T>(named name: String) throws -> T {
11-
if let value = self[name] as? T {
11+
12+
guard let anyValue = self[name] else
13+
{
14+
// this can never happen because arguments have already been enriched with default values
15+
preconditionFailure("Failed to retrieve value for parameter \(name)")
16+
}
17+
18+
// try direct type casting
19+
if let value = anyValue as? T {
1220
return value
13-
} else {
21+
}
22+
else if let caseType = T.self as? any CaseIterable.Type
23+
{
24+
guard let string = anyValue as? String else
25+
{
26+
throw MCPToolError.invalidArgumentType(
27+
parameterName: name,
28+
expectedType: "String",
29+
actualType: String(describing: Swift.type(of: anyValue))
30+
)
31+
}
32+
33+
let caseLabels = caseType.caseLabels
34+
35+
guard let index = caseLabels.firstIndex(of: string) else {
36+
37+
throw MCPToolError.invalidEnumValue(parameterName: name, expectedValues: caseLabels, actualValue: string)
38+
}
39+
40+
guard let allCases = caseType.allCases as? [T] else {
41+
// This can never happen because the result of CaseIterable is an array of the enum type
42+
preconditionFailure()
43+
}
44+
45+
// return the actual enum case value that matches the string label
46+
return allCases[index]
47+
}
48+
else {
1449
throw MCPToolError.invalidArgumentType(
1550
parameterName: name,
1651
expectedType: String(describing: T.self),
17-
actualType: String(describing: Swift.type(of: self[name] ?? "nil"))
52+
actualType: String(describing: Swift.type(of: anyValue))
1853
)
1954
}
2055
}
@@ -130,4 +165,4 @@ public extension Dictionary where Key == String, Value == Sendable {
130165
)
131166
}
132167
}
133-
}
168+
}

Sources/SwiftMCP/Extensions/MCPToolParameterInfo+JSONSchema.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import Foundation
1010
extension MCPToolParameterInfo {
1111

1212
var jsonSchema: JSONSchema {
13+
// If this is an enum parameter, return a string schema with enum values
14+
if let enumValues = enumValues {
15+
return JSONSchema.string(description: description, enumValues: enumValues)
16+
}
1317

1418
switch type.JSONSchemaType {
1519

Sources/SwiftMCP/Models/JSONSchema.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010
/// A simplified representation of JSON Schema for use in the macros
1111
public indirect enum JSONSchema: Sendable {
1212
/// A string schema
13-
case string(description: String? = nil)
13+
case string(description: String? = nil, enumValues: [String]? = nil)
1414

1515
/// A number schema
1616
case number(description: String? = nil)
@@ -23,4 +23,7 @@ public indirect enum JSONSchema: Sendable {
2323

2424
/// An object schema
2525
case object(properties: [String: JSONSchema], required: [String] = [], description: String? = nil)
26+
27+
/// An enum schema with possible values
28+
case `enum`(values: [String], description: String? = nil)
2629
}

Sources/SwiftMCP/Models/Tools/MCPTool.swift

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -101,24 +101,8 @@ extension MCPTool {
101101
// Add default values for parameters that are missing from the arguments dictionary
102102
for param in metadata.parameters {
103103
if enrichedArguments[param.name] == nil, let defaultValue = param.defaultValue {
104-
// Convert the default value to the appropriate type based on the parameter type
105-
switch param.type {
106-
case "Int":
107-
if let intValue = Int(defaultValue) {
108-
enrichedArguments[param.name] = intValue
109-
}
110-
case "Double", "Float":
111-
if let doubleValue = Double(defaultValue) {
112-
enrichedArguments[param.name] = doubleValue
113-
}
114-
case "Bool":
115-
if let boolValue = Bool(defaultValue) {
116-
enrichedArguments[param.name] = boolValue
117-
}
118-
default:
119-
// For string and other types, use the default value as is
120-
enrichedArguments[param.name] = defaultValue
121-
}
104+
105+
enrichedArguments[param.name] = defaultValue
122106
}
123107
}
124108

@@ -142,6 +126,8 @@ extension JSONSchema: Codable {
142126
case description
143127
/// The schema for array items
144128
case items
129+
/// The possible values for an enum schema
130+
case enumValues = "enum"
145131
}
146132

147133
/**
@@ -157,7 +143,8 @@ extension JSONSchema: Codable {
157143

158144
switch type {
159145
case "string":
160-
self = .string(description: description)
146+
let enumValues = try container.decodeIfPresent([String].self, forKey: .enumValues)
147+
self = .string(description: description, enumValues: enumValues)
161148
case "number":
162149
self = .number(description: description)
163150
case "boolean":
@@ -193,9 +180,12 @@ extension JSONSchema: Codable {
193180
var container = encoder.container(keyedBy: CodingKeys.self)
194181

195182
switch self {
196-
case .string(let description):
183+
case .string(let description, let enumValues):
197184
try container.encode("string", forKey: .type)
198185
try container.encodeIfPresent(description, forKey: .description)
186+
if let enumValues = enumValues {
187+
try container.encode(enumValues, forKey: .enumValues)
188+
}
199189
case .number(let description):
200190
try container.encode("number", forKey: .type)
201191
try container.encodeIfPresent(description, forKey: .description)
@@ -219,6 +209,10 @@ extension JSONSchema: Codable {
219209
}
220210

221211
try container.encodeIfPresent(description, forKey: .description)
212+
case .enum(let values, let description):
213+
try container.encode("string", forKey: .type)
214+
try container.encodeIfPresent(description, forKey: .description)
215+
try container.encode(values, forKey: .enumValues)
222216
}
223217
}
224218
}

Sources/SwiftMCP/Models/Tools/MCPToolError.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ public enum MCPToolError: Error {
88
/// An argument couldn't be cast to the correct type
99
case invalidArgumentType(parameterName: String, expectedType: String, actualType: String)
1010

11+
/// An argument couldn't be cast to the correct type
12+
case invalidEnumValue(parameterName: String, expectedValues: [String], actualValue: String)
13+
1114
/// The input is not a valid JSON dictionary
1215
case invalidJSONDictionary
1316

@@ -22,6 +25,9 @@ extension MCPToolError: LocalizedError {
2225
return "The tool '\(name)' was not found on the server"
2326
case .invalidArgumentType(let parameterName, let expectedType, let actualType):
2427
return "Parameter '\(parameterName)' expected type \(expectedType) but received type \(actualType)"
28+
case .invalidEnumValue(let parameterName, let expectedValues, let actualValue):
29+
let string = expectedValues.joined(separator: ", ")
30+
return "Parameter '\(parameterName)' expected one of [\(string)] but received \(actualValue)"
2531
case .invalidJSONDictionary:
2632
return "The input could not be parsed as a valid JSON dictionary"
2733
case .missingRequiredParameter(let parameterName):

Sources/SwiftMCP/Models/Tools/MCPToolParameterInfo.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ public struct MCPToolParameterInfo: Sendable {
2222
public let description: String?
2323

2424
/// An optional default value for the parameter
25-
public let defaultValue: String?
25+
public let defaultValue: Sendable?
26+
27+
/// The possible values for enum parameters
28+
public let enumValues: [String]?
2629

2730
/**
2831
Creates a new parameter info with the specified name, type, description, and default value.
@@ -33,12 +36,14 @@ public struct MCPToolParameterInfo: Sendable {
3336
- type: The type of the parameter
3437
- description: An optional description of the parameter
3538
- defaultValue: An optional default value for the parameter
39+
- enumValues: The possible values if this is an enum parameter
3640
*/
37-
public init(name: String, label: String, type: String, description: String? = nil, defaultValue: String? = nil) {
41+
public init(name: String, label: String, type: String, description: String? = nil, defaultValue: Sendable? = nil, enumValues: [String]? = nil) {
3842
self.name = name
3943
self.label = label
4044
self.type = type
4145
self.description = description
4246
self.defaultValue = defaultValue
47+
self.enumValues = enumValues
4348
}
4449
}

Sources/SwiftMCP/OpenAPI/OpenAPISpec.swift

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -170,30 +170,8 @@ struct OpenAPISpec: Codable {
170170
// Create input schema from parameters
171171
let inputSchema = JSONSchema.object(
172172
properties: metadata.parameters.reduce(into: [:]) { dict, param in
173-
let paramType = param.type.JSONSchemaType
174-
switch paramType {
175-
case "number":
176-
dict[param.name] = .number(description: param.description)
177-
case "boolean":
178-
dict[param.name] = .boolean(description: param.description)
179-
case "array":
180-
if let elementType = param.type.arrayElementType {
181-
let itemSchema: JSONSchema
182-
switch elementType.JSONSchemaType {
183-
case "number":
184-
itemSchema = .number()
185-
case "boolean":
186-
itemSchema = .boolean()
187-
default:
188-
itemSchema = .string()
189-
}
190-
dict[param.name] = .array(items: itemSchema, description: param.description)
191-
} else {
192-
dict[param.name] = .array(items: .string(), description: param.description)
193-
}
194-
default:
195-
dict[param.name] = .string(description: param.description)
196-
}
173+
// Use the parameter's JSONSchema directly
174+
dict[param.name] = param.jsonSchema
197175
},
198176
required: metadata.parameters.filter { $0.defaultValue == nil }.map { $0.name },
199177
description: metadata.description ?? "No description available"

0 commit comments

Comments
 (0)