Skip to content

Commit 3b505ee

Browse files
authored
Improve ergonomics of prompt and sampling message initialization (#120)
* Improve ergonomics of prompt and sampling message initialization * Deprecate previous / existing Message initializers
1 parent fac4df6 commit 3b505ee

File tree

5 files changed

+680
-171
lines changed

5 files changed

+680
-171
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -571,9 +571,9 @@ server.withMethodHandler(GetPrompt.self) { params in
571571

572572
let description = "Job interview for \(position) position at \(company)"
573573
let messages: [Prompt.Message] = [
574-
.init(role: .user, content: .text(text: "You are an interviewer for the \(position) position at \(company).")),
575-
.init(role: .user, content: .text(text: "Hello, I'm \(interviewee) and I'm here for the \(position) interview.")),
576-
.init(role: .assistant, content: .text(text: "Hi \(interviewee), welcome to \(company)! I'd like to start by asking about your background and experience."))
574+
.user("You are an interviewer for the \(position) position at \(company)."),
575+
.user("Hello, I'm \(interviewee) and I'm here for the \(position) interview."),
576+
.assistant("Hi \(interviewee), welcome to \(company)! I'd like to start by asking about your background and experience.")
577577
]
578578

579579
return .init(description: description, messages: messages)
@@ -609,7 +609,7 @@ let server = Server(
609609
do {
610610
let result = try await server.requestSampling(
611611
messages: [
612-
Sampling.Message(role: .user, content: .text("Analyze this data and suggest next steps"))
612+
.user("Analyze this data and suggest next steps")
613613
],
614614
systemPrompt: "You are a helpful data analyst",
615615
maxTokens: 150,

Sources/MCP/Server/Prompts.swift

Lines changed: 99 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,33 @@ public struct Prompt: Hashable, Codable, Sendable {
5353
/// The message content
5454
public let content: Content
5555

56+
/// Creates a message with the specified role and content
57+
@available(
58+
*, deprecated, message: "Use static factory methods .user(_:) or .assistant(_:) instead"
59+
)
5660
public init(role: Role, content: Content) {
5761
self.role = role
5862
self.content = content
5963
}
6064

65+
/// Private initializer for convenience methods to avoid deprecation warnings
66+
private init(_role role: Role, _content content: Content) {
67+
self.role = role
68+
self.content = content
69+
}
70+
71+
/// Creates a user message with the specified content
72+
public static func user(_ content: Content) -> Message {
73+
return Message(_role: .user, _content: content)
74+
}
75+
76+
/// Creates an assistant message with the specified content
77+
public static func assistant(_ content: Content) -> Message {
78+
return Message(_role: .assistant, _content: content)
79+
}
80+
6181
/// Content types for messages
62-
public enum Content: Hashable, Codable, Sendable {
82+
public enum Content: Hashable, Sendable {
6383
/// Text content
6484
case text(text: String)
6585
/// Image content
@@ -68,64 +88,6 @@ public struct Prompt: Hashable, Codable, Sendable {
6888
case audio(data: String, mimeType: String)
6989
/// Embedded resource content
7090
case resource(uri: String, mimeType: String, text: String?, blob: String?)
71-
72-
private enum CodingKeys: String, CodingKey {
73-
case type, text, data, mimeType, uri, blob
74-
}
75-
76-
public func encode(to encoder: Encoder) throws {
77-
var container = encoder.container(keyedBy: CodingKeys.self)
78-
79-
switch self {
80-
case .text(let text):
81-
try container.encode("text", forKey: .type)
82-
try container.encode(text, forKey: .text)
83-
case .image(let data, let mimeType):
84-
try container.encode("image", forKey: .type)
85-
try container.encode(data, forKey: .data)
86-
try container.encode(mimeType, forKey: .mimeType)
87-
case .audio(let data, let mimeType):
88-
try container.encode("audio", forKey: .type)
89-
try container.encode(data, forKey: .data)
90-
try container.encode(mimeType, forKey: .mimeType)
91-
case .resource(let uri, let mimeType, let text, let blob):
92-
try container.encode("resource", forKey: .type)
93-
try container.encode(uri, forKey: .uri)
94-
try container.encode(mimeType, forKey: .mimeType)
95-
try container.encodeIfPresent(text, forKey: .text)
96-
try container.encodeIfPresent(blob, forKey: .blob)
97-
}
98-
}
99-
100-
public init(from decoder: Decoder) throws {
101-
let container = try decoder.container(keyedBy: CodingKeys.self)
102-
let type = try container.decode(String.self, forKey: .type)
103-
104-
switch type {
105-
case "text":
106-
let text = try container.decode(String.self, forKey: .text)
107-
self = .text(text: text)
108-
case "image":
109-
let data = try container.decode(String.self, forKey: .data)
110-
let mimeType = try container.decode(String.self, forKey: .mimeType)
111-
self = .image(data: data, mimeType: mimeType)
112-
case "audio":
113-
let data = try container.decode(String.self, forKey: .data)
114-
let mimeType = try container.decode(String.self, forKey: .mimeType)
115-
self = .audio(data: data, mimeType: mimeType)
116-
case "resource":
117-
let uri = try container.decode(String.self, forKey: .uri)
118-
let mimeType = try container.decode(String.self, forKey: .mimeType)
119-
let text = try container.decodeIfPresent(String.self, forKey: .text)
120-
let blob = try container.decodeIfPresent(String.self, forKey: .blob)
121-
self = .resource(uri: uri, mimeType: mimeType, text: text, blob: blob)
122-
default:
123-
throw DecodingError.dataCorruptedError(
124-
forKey: .type,
125-
in: container,
126-
debugDescription: "Unknown content type")
127-
}
128-
}
12991
}
13092
}
13193

@@ -156,6 +118,84 @@ public struct Prompt: Hashable, Codable, Sendable {
156118
}
157119
}
158120

121+
// MARK: - Codable
122+
123+
extension Prompt.Message.Content: Codable {
124+
private enum CodingKeys: String, CodingKey {
125+
case type, text, data, mimeType, uri, blob
126+
}
127+
128+
public func encode(to encoder: Encoder) throws {
129+
var container = encoder.container(keyedBy: CodingKeys.self)
130+
131+
switch self {
132+
case .text(let text):
133+
try container.encode("text", forKey: .type)
134+
try container.encode(text, forKey: .text)
135+
case .image(let data, let mimeType):
136+
try container.encode("image", forKey: .type)
137+
try container.encode(data, forKey: .data)
138+
try container.encode(mimeType, forKey: .mimeType)
139+
case .audio(let data, let mimeType):
140+
try container.encode("audio", forKey: .type)
141+
try container.encode(data, forKey: .data)
142+
try container.encode(mimeType, forKey: .mimeType)
143+
case .resource(let uri, let mimeType, let text, let blob):
144+
try container.encode("resource", forKey: .type)
145+
try container.encode(uri, forKey: .uri)
146+
try container.encode(mimeType, forKey: .mimeType)
147+
try container.encodeIfPresent(text, forKey: .text)
148+
try container.encodeIfPresent(blob, forKey: .blob)
149+
}
150+
}
151+
152+
public init(from decoder: Decoder) throws {
153+
let container = try decoder.container(keyedBy: CodingKeys.self)
154+
let type = try container.decode(String.self, forKey: .type)
155+
156+
switch type {
157+
case "text":
158+
let text = try container.decode(String.self, forKey: .text)
159+
self = .text(text: text)
160+
case "image":
161+
let data = try container.decode(String.self, forKey: .data)
162+
let mimeType = try container.decode(String.self, forKey: .mimeType)
163+
self = .image(data: data, mimeType: mimeType)
164+
case "audio":
165+
let data = try container.decode(String.self, forKey: .data)
166+
let mimeType = try container.decode(String.self, forKey: .mimeType)
167+
self = .audio(data: data, mimeType: mimeType)
168+
case "resource":
169+
let uri = try container.decode(String.self, forKey: .uri)
170+
let mimeType = try container.decode(String.self, forKey: .mimeType)
171+
let text = try container.decodeIfPresent(String.self, forKey: .text)
172+
let blob = try container.decodeIfPresent(String.self, forKey: .blob)
173+
self = .resource(uri: uri, mimeType: mimeType, text: text, blob: blob)
174+
default:
175+
throw DecodingError.dataCorruptedError(
176+
forKey: .type,
177+
in: container,
178+
debugDescription: "Unknown content type")
179+
}
180+
}
181+
}
182+
183+
// MARK: - ExpressibleByStringLiteral
184+
185+
extension Prompt.Message.Content: ExpressibleByStringLiteral {
186+
public init(stringLiteral value: String) {
187+
self = .text(text: value)
188+
}
189+
}
190+
191+
// MARK: - ExpressibleByStringInterpolation
192+
193+
extension Prompt.Message.Content: ExpressibleByStringInterpolation {
194+
public init(stringInterpolation: DefaultStringInterpolation) {
195+
self = .text(text: String(stringInterpolation: stringInterpolation))
196+
}
197+
}
198+
159199
// MARK: -
160200

161201
/// To retrieve available prompts, clients send a `prompts/list` request.

Sources/MCP/Server/Sampling.swift

Lines changed: 80 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -21,54 +21,37 @@ public enum Sampling {
2121
/// The message content
2222
public let content: Content
2323

24+
/// Creates a message with the specified role and content
25+
@available(
26+
*, deprecated, message: "Use static factory methods .user(_:) or .assistant(_:) instead"
27+
)
2428
public init(role: Role, content: Content) {
2529
self.role = role
2630
self.content = content
2731
}
2832

33+
/// Private initializer for convenience methods to avoid deprecation warnings
34+
private init(_role role: Role, _content content: Content) {
35+
self.role = role
36+
self.content = content
37+
}
38+
39+
/// Creates a user message with the specified content
40+
public static func user(_ content: Content) -> Message {
41+
return Message(_role: .user, _content: content)
42+
}
43+
44+
/// Creates an assistant message with the specified content
45+
public static func assistant(_ content: Content) -> Message {
46+
return Message(_role: .assistant, _content: content)
47+
}
48+
2949
/// Content types for sampling messages
30-
public enum Content: Hashable, Codable, Sendable {
50+
public enum Content: Hashable, Sendable {
3151
/// Text content
3252
case text(String)
3353
/// Image content
3454
case image(data: String, mimeType: String)
35-
36-
private enum CodingKeys: String, CodingKey {
37-
case type, text, data, mimeType
38-
}
39-
40-
public init(from decoder: Decoder) throws {
41-
let container = try decoder.container(keyedBy: CodingKeys.self)
42-
let type = try container.decode(String.self, forKey: .type)
43-
44-
switch type {
45-
case "text":
46-
let text = try container.decode(String.self, forKey: .text)
47-
self = .text(text)
48-
case "image":
49-
let data = try container.decode(String.self, forKey: .data)
50-
let mimeType = try container.decode(String.self, forKey: .mimeType)
51-
self = .image(data: data, mimeType: mimeType)
52-
default:
53-
throw DecodingError.dataCorruptedError(
54-
forKey: .type, in: container,
55-
debugDescription: "Unknown sampling message content type")
56-
}
57-
}
58-
59-
public func encode(to encoder: Encoder) throws {
60-
var container = encoder.container(keyedBy: CodingKeys.self)
61-
62-
switch self {
63-
case .text(let text):
64-
try container.encode("text", forKey: .type)
65-
try container.encode(text, forKey: .text)
66-
case .image(let data, let mimeType):
67-
try container.encode("image", forKey: .type)
68-
try container.encode(data, forKey: .data)
69-
try container.encode(mimeType, forKey: .mimeType)
70-
}
71-
}
7255
}
7356
}
7457

@@ -127,6 +110,65 @@ public enum Sampling {
127110
}
128111
}
129112

113+
// MARK: - Codable
114+
115+
extension Sampling.Message.Content: Codable {
116+
private enum CodingKeys: String, CodingKey {
117+
case type, text, data, mimeType
118+
}
119+
120+
public init(from decoder: Decoder) throws {
121+
let container = try decoder.container(keyedBy: CodingKeys.self)
122+
let type = try container.decode(String.self, forKey: .type)
123+
124+
switch type {
125+
case "text":
126+
let text = try container.decode(String.self, forKey: .text)
127+
self = .text(text)
128+
case "image":
129+
let data = try container.decode(String.self, forKey: .data)
130+
let mimeType = try container.decode(String.self, forKey: .mimeType)
131+
self = .image(data: data, mimeType: mimeType)
132+
default:
133+
throw DecodingError.dataCorruptedError(
134+
forKey: .type, in: container,
135+
debugDescription: "Unknown sampling message content type")
136+
}
137+
}
138+
139+
public func encode(to encoder: Encoder) throws {
140+
var container = encoder.container(keyedBy: CodingKeys.self)
141+
142+
switch self {
143+
case .text(let text):
144+
try container.encode("text", forKey: .type)
145+
try container.encode(text, forKey: .text)
146+
case .image(let data, let mimeType):
147+
try container.encode("image", forKey: .type)
148+
try container.encode(data, forKey: .data)
149+
try container.encode(mimeType, forKey: .mimeType)
150+
}
151+
}
152+
}
153+
154+
// MARK: - ExpressibleByStringLiteral
155+
156+
extension Sampling.Message.Content: ExpressibleByStringLiteral {
157+
public init(stringLiteral value: String) {
158+
self = .text(value)
159+
}
160+
}
161+
162+
// MARK: - ExpressibleByStringInterpolation
163+
164+
extension Sampling.Message.Content: ExpressibleByStringInterpolation {
165+
public init(stringInterpolation: DefaultStringInterpolation) {
166+
self = .text(String(stringInterpolation: stringInterpolation))
167+
}
168+
}
169+
170+
// MARK: -
171+
130172
/// To request sampling from a client, servers send a `sampling/createMessage` request.
131173
/// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/sampling#how-sampling-works
132174
public enum CreateSamplingMessage: Method {

0 commit comments

Comments
 (0)