Skip to content

Commit 52de469

Browse files
committed
Add proxy support for MCP resources and prompts
1 parent dad8f89 commit 52de469

File tree

12 files changed

+595
-97
lines changed

12 files changed

+595
-97
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ try await tcpTransport.run()
154154
## Typed Client Proxy
155155

156156
SwiftMCP can generate a typed client proxy for any server. This proxy mirrors the
157-
`@MCPTool` signatures and forwards calls through `MCPServerProxy`. Enable it with
158-
`generateClient: true` on the server macro.
157+
`@MCPTool`, `@MCPResource`, and `@MCPPrompt` signatures and forwards calls through
158+
`MCPServerProxy`. Enable it with `generateClient: true` on the server macro.
159159

160160
```swift
161161
@MCPServer(generateClient: true)
@@ -191,7 +191,8 @@ Notes:
191191

192192
Use `SwiftMCPUtility generate-proxy` to create a client proxy for any MCP server,
193193
including non-SwiftMCP servers. The generated proxy calls `MCPServerProxy` under
194-
the hood and always returns `String` because MCP does not include output schemas.
194+
the hood, generates typed tool methods from MCP tool schemas, and adds first-class
195+
resource/prompt convenience APIs when the server advertises those capabilities.
195196
Input parameters are inferred from the server's tool schemas, and only string
196197
formats such as `date-time`, `uri`, `uuid`, and `byte` can be mapped to native
197198
types (`Date`, `URL`, `UUID`, `Data`).

Sources/SwiftMCP/Client/MCPServerProxy.swift

Lines changed: 121 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -131,47 +131,8 @@ public final actor MCPServerProxy: Sendable {
131131
return cachedTools
132132
}
133133

134-
let requestId = nextRequestID()
135-
let request = JSONRPCMessage.request(id: requestId, method: "tools/list")
136-
let response = try await send(request)
137-
138-
guard case let .response(respData) = response, let result = respData.result else {
139-
throw MCPServerProxyError.communicationError("Invalid response type for tools/list")
140-
}
141-
142-
if let isError = result["isError"]?.value as? Bool, isError {
143-
throw MCPServerProxyError.communicationError("Server does not provide any tools")
144-
}
145-
146-
guard let toolsData = result["tools"]?.value as? [[String: Any]] else {
147-
throw MCPServerProxyError.communicationError("Invalid response format for tools/list")
148-
}
149-
150-
let tools = try toolsData.compactMap { toolData -> MCPTool? in
151-
guard let name = toolData["name"] as? String,
152-
let inputSchema = toolData["inputSchema"] as? [String: Any] else {
153-
return nil
154-
}
155-
156-
let description = toolData["description"] as? String
157-
let schema = try JSONDecoder().decode(JSONSchema.self, from: JSONSerialization.data(withJSONObject: inputSchema))
158-
let outputSchema: JSONSchema?
159-
if let outputSchemaData = toolData["outputSchema"] as? [String: Any] {
160-
outputSchema = try JSONDecoder().decode(JSONSchema.self, from: JSONSerialization.data(withJSONObject: outputSchemaData))
161-
} else {
162-
outputSchema = nil
163-
}
164-
165-
// Parse annotations if present
166-
let annotations: MCPToolAnnotations?
167-
if let annotationsData = toolData["annotations"] as? [String: Any] {
168-
annotations = try JSONDecoder().decode(MCPToolAnnotations.self, from: JSONSerialization.data(withJSONObject: annotationsData))
169-
} else {
170-
annotations = nil
171-
}
172-
173-
return MCPTool(name: name, description: description, inputSchema: schema, outputSchema: outputSchema, annotations: annotations)
174-
}
134+
let result = try await requestResult(method: "tools/list")
135+
let tools: [MCPTool] = try decodeResultField("tools", from: result, method: "tools/list")
175136

176137
if cacheToolsList {
177138
cachedTools = tools
@@ -180,6 +141,48 @@ public final actor MCPServerProxy: Sendable {
180141
return tools
181142
}
182143

144+
/// Lists all static resources available from the server.
145+
public func listResources() async throws -> [SimpleResource] {
146+
let result = try await requestResult(method: "resources/list")
147+
return try decodeResultField("resources", from: result, method: "resources/list")
148+
}
149+
150+
/// Lists all resource templates available from the server.
151+
public func listResourceTemplates() async throws -> [SimpleResourceTemplate] {
152+
let result = try await requestResult(method: "resources/templates/list")
153+
return try decodeResultField("resourceTemplates", from: result, method: "resources/templates/list")
154+
}
155+
156+
/// Reads a resource at the specified URI.
157+
public func readResource(uri: URL) async throws -> [GenericResourceContent] {
158+
let result = try await requestResult(
159+
method: "resources/read",
160+
params: ["uri": AnyCodable(uri.absoluteString)]
161+
)
162+
return try decodeResultField("contents", from: result, method: "resources/read")
163+
}
164+
165+
/// Lists all prompts available from the server.
166+
public func listPrompts() async throws -> [Prompt] {
167+
let result = try await requestResult(method: "prompts/list")
168+
return try decodeResultField("prompts", from: result, method: "prompts/list")
169+
}
170+
171+
/// Gets a prompt by name with optional arguments.
172+
public func getPrompt(
173+
name: String,
174+
arguments: [String: any Sendable] = [:]
175+
) async throws -> PromptResult {
176+
let result = try await requestResult(
177+
method: "prompts/get",
178+
params: [
179+
"name": AnyCodable(name),
180+
"arguments": AnyCodable(arguments.mapValues(AnyCodable.init))
181+
]
182+
)
183+
return try decodeResultField("self", from: result, method: "prompts/get", as: PromptResult.self)
184+
}
185+
183186
/// Calls a tool by name on the connected MCP server with the provided arguments.
184187
public func callTool(
185188
_ name: String,
@@ -205,18 +208,21 @@ public final actor MCPServerProxy: Sendable {
205208
let request = JSONRPCMessage.request(id: requestId, method: "tools/call", params: params)
206209
let responseMessage = try await send(request)
207210

208-
guard case let .response(responseData) = responseMessage, let result = responseData.result else {
211+
let result: [String: AnyCodable]
212+
switch responseMessage {
213+
case .response(let responseData):
214+
guard let responseResult = responseData.result else {
215+
throw MCPServerProxyError.communicationError("Invalid response type for tools/call, expected JSONRPCResponse")
216+
}
217+
result = responseResult
218+
case .errorResponse(let errorResponse):
219+
throw MCPServerProxyError.toolError(errorResponse.error.message)
220+
default:
209221
throw MCPServerProxyError.communicationError("Invalid response type for tools/call, expected JSONRPCResponse")
210222
}
211223

212224
if let isError = result["isError"]?.value as? Bool, isError {
213-
var errorMessage = "Tool call failed with an unspecified error."
214-
if let contentArray = result["content"]?.value as? [Any],
215-
let firstContent = contentArray.first as? [String: Any],
216-
let text = firstContent["text"] as? String {
217-
errorMessage = text
218-
}
219-
throw MCPServerProxyError.toolError(errorMessage)
225+
throw MCPServerProxyError.toolError(errorMessage(from: result) ?? "Tool call failed with an unspecified error.")
220226
}
221227

222228
guard let contentValue = result["content"]?.value else {
@@ -714,6 +720,72 @@ public final actor MCPServerProxy: Sendable {
714720
return nil
715721
}
716722

723+
private func requestResult(
724+
method: String,
725+
params: [String: AnyCodable]? = nil
726+
) async throws -> [String: AnyCodable] {
727+
let requestId = nextRequestID()
728+
let request = JSONRPCMessage.request(id: requestId, method: method, params: params)
729+
let response = try await send(request)
730+
731+
switch response {
732+
case .response(let responseData):
733+
guard let result = responseData.result else {
734+
throw MCPServerProxyError.communicationError("Invalid response type for \(method)")
735+
}
736+
if let isError = result["isError"]?.value as? Bool, isError {
737+
throw MCPServerProxyError.communicationError(errorMessage(from: result) ?? "Request failed for \(method)")
738+
}
739+
return result
740+
case .errorResponse(let errorResponse):
741+
throw MCPServerProxyError.communicationError(errorResponse.error.message)
742+
default:
743+
throw MCPServerProxyError.communicationError("Invalid response type for \(method)")
744+
}
745+
}
746+
747+
private func decodeResultField<T: Decodable>(
748+
_ field: String,
749+
from result: [String: AnyCodable],
750+
method: String,
751+
as type: T.Type = T.self
752+
) throws -> T {
753+
let value: AnyCodable
754+
if field == "self" {
755+
value = AnyCodable(result)
756+
} else if let fieldValue = result[field] {
757+
value = fieldValue
758+
} else {
759+
throw MCPServerProxyError.communicationError("Invalid response format for \(method)")
760+
}
761+
762+
let encoder = JSONEncoder()
763+
encoder.dateEncodingStrategy = .iso8601WithTimeZone
764+
let data = try encoder.encode(value)
765+
766+
let decoder = JSONDecoder()
767+
decoder.dateDecodingStrategy = .iso8601WithTimeZone
768+
return try decoder.decode(type, from: data)
769+
}
770+
771+
private func errorMessage(from result: [String: AnyCodable]) -> String? {
772+
if let message = stringValue(result["message"]) {
773+
return message
774+
}
775+
if let contentValue = result["content"]?.value {
776+
let contentArray: [Any]
777+
if let array = contentValue as? [Any] {
778+
contentArray = array
779+
} else if let array = contentValue as? [AnyCodable] {
780+
contentArray = array.map(\.value)
781+
} else {
782+
contentArray = []
783+
}
784+
return extractTextPayload(from: contentArray)
785+
}
786+
return nil
787+
}
788+
717789

718790
private func extractServerDescription(from result: [String: AnyCodable]) -> String? {
719791
guard let serverInfoValue = result["serverInfo"]?.value else {

Sources/SwiftMCP/Models/Prompts/Prompt.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import AnyCodable
33

44
/// Represents a prompt that can be provided by an MCP server
5-
public struct Prompt: Encodable, Sendable {
5+
public struct Prompt: Codable, Sendable {
66
/// Unique name of the prompt
77
public let name: String
88

@@ -24,6 +24,28 @@ extension Prompt {
2424
case name, description, arguments
2525
}
2626

27+
private struct ArgumentPayload: Codable {
28+
let name: String
29+
let description: String?
30+
let required: Bool?
31+
}
32+
33+
public init(from decoder: Decoder) throws {
34+
let container = try decoder.container(keyedBy: CodingKeys.self)
35+
name = try container.decode(String.self, forKey: .name)
36+
description = try container.decodeIfPresent(String.self, forKey: .description)
37+
38+
let args = try container.decodeIfPresent([ArgumentPayload].self, forKey: .arguments) ?? []
39+
arguments = args.map { argument in
40+
MCPParameterInfo(
41+
name: argument.name,
42+
type: String.self,
43+
description: argument.description?.isEmpty == false ? argument.description : nil,
44+
isRequired: argument.required ?? false
45+
)
46+
}
47+
}
48+
2749
public func encode(to encoder: Encoder) throws {
2850
var container = encoder.container(keyedBy: CodingKeys.self)
2951
try container.encode(name, forKey: .name)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Foundation
2+
3+
/// The result returned by `prompts/get`.
4+
public struct PromptResult: Codable, Sendable {
5+
public let description: String?
6+
public let messages: [PromptMessage]
7+
8+
public init(description: String? = nil, messages: [PromptMessage]) {
9+
self.description = description
10+
self.messages = messages
11+
}
12+
}

Sources/SwiftMCP/Models/Resources/MCPResourceMetadata.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,11 @@ public struct SimpleResourceTemplate: MCPResourceTemplate {
106106
public let name: String
107107
public let description: String?
108108
public let mimeType: String?
109-
}
109+
110+
public init(uriTemplate: String, name: String, description: String? = nil, mimeType: String? = nil) {
111+
self.uriTemplate = uriTemplate
112+
self.name = name
113+
self.description = description
114+
self.mimeType = mimeType
115+
}
116+
}

Sources/SwiftMCP/SwiftMCP.docc/Tutorials/BuildingAnMCPClient.tutorial

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
@Tutorial(time: 15) {
22
@Intro(title: "Building an MCP Client") {
3-
Learn how to generate a typed client proxy from a SwiftMCP server and call tools
3+
Learn how to generate a typed client proxy from a SwiftMCP server and call tools, resources, and prompts
44
over JSON-RPC using `MCPServerProxy`.
55

6-
The generated client mirrors your `@MCPTool` signatures and includes the
6+
The generated client mirrors your `@MCPTool`, `@MCPResource`, and `@MCPPrompt` signatures and includes the
77
same documentation for autocompletion.
88

99
@Image(source: "placeholder.png", alt: "Illustration showing a SwiftMCP client calling tools")
@@ -13,14 +13,14 @@
1313
@ContentAndMedia {
1414
Opt in to client generation by passing `generateClient: true` to the
1515
`@MCPServer` macro. This creates a nested `Client` type that mirrors
16-
the `@MCPTool` functions.
16+
the server's MCP tools, resources, and prompts.
1717

1818
@Image(source: "placeholder.png", alt: "Illustration showing a generated client nested type")
1919
}
2020

2121
@Steps {
2222
@Step {
23-
Create a server with tools and enable client generation.
23+
Create a server with MCP surfaces and enable client generation.
2424

2525
@Code(name: "Calculator.swift", file: "07-client-server.swift")
2626
}
@@ -30,7 +30,7 @@
3030
@Section(title: "Connect and Call Tools") {
3131
@ContentAndMedia {
3232
Use `MCPServerProxy` to connect to the server, then instantiate the
33-
generated client and call tools with native types.
33+
generated client and call tools, resources, and prompts with native types.
3434

3535
Client methods always throw to surface transport or server errors.
3636

@@ -45,7 +45,7 @@
4545
}
4646

4747
@Step {
48-
Call tools with native input and output types.
48+
Call generated client APIs with native input and output types.
4949

5050
@Code(name: "Client.swift", file: "09-client-calls.swift")
5151
}

0 commit comments

Comments
 (0)