Skip to content

Commit 1812c5c

Browse files
committed
Added experimental resources support, ping command
1 parent 772d256 commit 1812c5c

File tree

9 files changed

+530
-3
lines changed

9 files changed

+530
-3
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Foundation
2+
3+
/// Protocol defining the requirements for an MCP resource
4+
public protocol MCPResource: Codable {
5+
/// The URI of the resource
6+
var uri: URL { get }
7+
8+
/// The name of the resource
9+
var name: String { get }
10+
11+
/// The description of the resource
12+
var description: String { get }
13+
14+
/// The MIME type of the resource
15+
var mimeType: String { get }
16+
}
17+
18+
19+
/// Errors that can occur when working with MCPResources
20+
public enum MCPResourceError: Error, CustomStringConvertible {
21+
/// The URI string is invalid
22+
case invalidURI(String)
23+
24+
/// A description of the error
25+
public var description: String {
26+
switch self {
27+
case .invalidURI(let uriString):
28+
return "Invalid URI: \(uriString)"
29+
}
30+
}
31+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Foundation
2+
3+
/// Protocol defining the requirements for MCP resource content
4+
public protocol MCPResourceContent: Codable {
5+
/// The URI of the resource
6+
var uri: URL { get }
7+
8+
/// The MIME type of the resource (optional)
9+
var mimeType: String? { get }
10+
11+
/// The text content of the resource (if it's a text resource)
12+
var text: String? { get }
13+
14+
/// The binary content of the resource (if it's a binary resource)
15+
var blob: Data? { get }
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Foundation
2+
3+
/// Protocol defining the requirements for an MCP resource template
4+
public protocol MCPResourceTemplate: Codable {
5+
/// The URI of the resource
6+
var uriTemplate: URL { get }
7+
8+
/// The name of the resource
9+
var name: String { get }
10+
11+
/// The description of the resource
12+
var description: String { get }
13+
14+
/// The MIME type of the resource
15+
var mimeType: String { get }
16+
}

Sources/SwiftMCP/Protocols/MCPServer.swift

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ public protocol MCPServer {
66
/// Returns an array of all MCP tools defined in this type
77
var mcpTools: [MCPTool] { get }
88

9+
/// Returns an array of all MCP resources defined in this type
10+
var mcpResources: [MCPResource] { get }
11+
12+
/// Returns an array of all MCP resource templates defined in this type
13+
var mcpResourceTemplates: [MCPResourceTemplate] { get }
14+
15+
916
/// Calls a tool by name with the provided arguments
1017
/// - Parameters:
1118
/// - name: The name of the tool to call
@@ -14,12 +21,25 @@ public protocol MCPServer {
1421
/// - Throws: MCPToolError if the tool doesn't exist or cannot be called
1522
func callTool(_ name: String, arguments: [String: Any]) throws -> Any
1623

24+
/// Gets a resource by URI
25+
/// - Parameter uri: The URI of the resource to get
26+
/// - Returns: The resource content, or nil if the resource doesn't exist
27+
/// - Throws: MCPResourceError if there's an error getting the resource
28+
func getResource(uri: URL) throws -> MCPResourceContent?
29+
1730
/// Handles a JSON-RPC request
1831
/// - Parameter request: The JSON-RPC request to handle
1932
/// - Returns: The response as a string, or nil if no response should be sent
2033
func handleRequest(_ request: JSONRPCRequest) -> Codable?
2134
}
2235

36+
public enum MCPResourceKind
37+
{
38+
case text(String)
39+
40+
case data(Data)
41+
}
42+
2343
// MARK: - Default Implementations
2444
public extension MCPServer {
2545
/// Handles a JSON-RPC request with default implementation
@@ -35,9 +55,21 @@ public extension MCPServer {
3555
case "notifications/initialized":
3656
return nil
3757

58+
case "ping":
59+
return createPingResponse(id: request.id ?? 0)
60+
3861
case "tools/list":
3962
return createToolsResponse(id: request.id ?? 0)
4063

64+
case "resources/list":
65+
return createResourcesListResponse(id: request.id ?? 0)
66+
67+
case "resources/templates/list":
68+
return createResourceTemplatesListResponse(id: request.id ?? 0)
69+
70+
case "resources/read":
71+
return createResourcesReadResponse(id: request.id ?? 0, request: request)
72+
4173
case "tools/call":
4274
return handleToolCall(request)
4375

@@ -54,6 +86,7 @@ public extension MCPServer {
5486
"protocolVersion": "2024-11-05",
5587
"capabilities": [
5688
"experimental": [:],
89+
"resources": ["listChanged": false],
5790
"tools": ["listChanged": false]
5891
],
5992
"serverInfo": [
@@ -65,6 +98,27 @@ public extension MCPServer {
6598
return JSONRPC.Response(id: .number(id), result: .init(responseDict))
6699
}
67100

101+
/// Creates a resources list response
102+
/// - Parameter id: The request ID
103+
/// - Returns: The resources list response
104+
func createResourcesListResponse(id: Int) -> JSONRPC.Response {
105+
// Convert MCPResource objects to dictionaries
106+
let resourceDicts = mcpResources.map { resource -> [String: Any] in
107+
return [
108+
"uri": resource.uri.absoluteString,
109+
"name": resource.name,
110+
"description": resource.description,
111+
"mimeType": resource.mimeType
112+
]
113+
}
114+
115+
let resourcesList: [String: Any] = [
116+
"resources": resourceDicts
117+
]
118+
119+
return JSONRPC.Response(id: .number(id), result: .init(resourcesList))
120+
}
121+
68122
/// Creates a tools response
69123
/// - Parameter id: The request ID
70124
/// - Returns: The tools response
@@ -130,4 +184,115 @@ public extension MCPServer {
130184
private var serverVersion: String {
131185
Mirror(reflecting: self).children.first(where: { $0.label == "__mcpServerVersion" })?.value as? String ?? "UnknownVersion"
132186
}
133-
}
187+
188+
/// Creates a resources read response
189+
/// - Parameters:
190+
/// - id: The request ID
191+
/// - request: The original JSON-RPC request
192+
/// - Returns: The resources read response
193+
func createResourcesReadResponse(id: Int, request: JSONRPCRequest) -> JSONRPC.Response {
194+
// Extract the URI from the request params
195+
guard let uriString = request.params?["uri"]?.value as? String,
196+
let uri = URL(string: uriString) else {
197+
// If no URI is provided or it's invalid, return an error
198+
let errorDict: [String: Any] = [
199+
"error": "Invalid or missing URI parameter"
200+
]
201+
return JSONRPC.Response(id: .number(id), result: .init(errorDict))
202+
}
203+
204+
do {
205+
// Try to get the resource content
206+
if let resourceContent = try getResource(uri: uri) {
207+
// Convert MCPResourceContent to dictionary
208+
var contentDict: [String: Any] = [
209+
"uri": resourceContent.uri.absoluteString
210+
]
211+
212+
// Add optional fields if they exist
213+
if let mimeType = resourceContent.mimeType {
214+
contentDict["mimeType"] = mimeType
215+
}
216+
217+
if let text = resourceContent.text {
218+
contentDict["text"] = text
219+
}
220+
221+
if let blob = resourceContent.blob {
222+
// Convert binary data to base64 string
223+
contentDict["blob"] = blob.base64EncodedString()
224+
}
225+
226+
// Create the response
227+
let responseDict: [String: Any] = [
228+
"contents": [contentDict]
229+
]
230+
231+
return JSONRPC.Response(id: .number(id), result: .init(responseDict))
232+
} else {
233+
// Resource not found
234+
let errorDict: [String: Any] = [
235+
"error": "Resource not found: \(uri.absoluteString)"
236+
]
237+
return JSONRPC.Response(id: .number(id), result: .init(errorDict))
238+
}
239+
} catch {
240+
// Error getting resource
241+
let errorDict: [String: Any] = [
242+
"error": "Error getting resource: \(error.localizedDescription)"
243+
]
244+
return JSONRPC.Response(id: .number(id), result: .init(errorDict))
245+
}
246+
}
247+
248+
/// Creates a resource templates list response
249+
/// - Parameter id: The request ID
250+
/// - Returns: The resource templates list response
251+
func createResourceTemplatesListResponse(id: Int) -> JSONRPC.Response {
252+
// Convert MCPResourceTemplate objects to dictionaries
253+
let templateDicts = mcpResourceTemplates.map { template -> [String: Any] in
254+
return [
255+
"uriTemplate": template.uriTemplate.absoluteString,
256+
"name": template.name,
257+
"description": template.description,
258+
"mimeType": template.mimeType
259+
]
260+
}
261+
262+
let templatesResponse: [String: Any] = [
263+
"resourceTemplates": templateDicts
264+
]
265+
266+
return JSONRPC.Response(id: .number(id), result: .init(templatesResponse))
267+
}
268+
269+
/// Creates a ping response
270+
/// - Parameter id: The request ID
271+
/// - Returns: The ping response
272+
func createPingResponse(id: Int) -> JSONRPC.Response {
273+
// Create an empty result object
274+
let emptyResult: [String: Any] = [:]
275+
276+
// Return a response with the empty result
277+
return JSONRPC.Response(id: .number(id), result: .init(emptyResult))
278+
}
279+
280+
/// Default implementation for mcpResources
281+
var mcpResources: [MCPResource] {
282+
// By default, return an empty array
283+
// Implementations can override this to provide actual resources
284+
return []
285+
}
286+
287+
/// Default implementation for mcpResources
288+
var mcpResourceTemplates: [MCPResourceTemplate] {
289+
// By default, return an empty array
290+
// Implementations can override this to provide actual resources
291+
return []
292+
}
293+
294+
/// Default implementation
295+
func getResource(uri: URL) throws -> MCPResourceContent? {
296+
return nil
297+
}
298+
}

Sources/SwiftMCPDemo/Calculator.swift

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import Foundation
22
import SwiftMCP
33

4-
@MCPServer(name: "Custom Calculator", version: "0.1")
5-
struct Calculator {
4+
@MCPServer(name: "Calculator", version: "1.0.0")
5+
class Calculator {
66
/// Adds two integers and returns their sum
77
/// - Parameter a: First number to add
88
/// - Parameter b: Second number to add
@@ -64,4 +64,77 @@ struct Calculator {
6464
func ping() -> String {
6565
return "pong"
6666
}
67+
68+
/// Returns an array of all MCP resources defined in this type
69+
var mcpResources: [MCPResource] {
70+
// Get the Downloads folder URL
71+
guard let downloadsURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
72+
logToStderr("Could not get Downloads folder URL")
73+
return []
74+
}
75+
76+
do {
77+
// List all files in the Downloads folder
78+
let fileURLs = try FileManager.default.contentsOfDirectory(
79+
at: downloadsURL,
80+
includingPropertiesForKeys: [.isRegularFileKey, .nameKey, .fileSizeKey],
81+
options: [.skipsHiddenFiles]
82+
)
83+
84+
// Filter to only include regular files
85+
let regularFileURLs = fileURLs.filter { url in
86+
do {
87+
let resourceValues = try url.resourceValues(forKeys: [.isRegularFileKey])
88+
return resourceValues.isRegularFile ?? false
89+
} catch {
90+
return false
91+
}
92+
}
93+
94+
// Create FileResource objects for each file
95+
return regularFileURLs.map { fileURL in
96+
// Get file attributes for description
97+
let fileAttributes: String
98+
do {
99+
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
100+
let fileSize = attributes[.size] as? Int64 ?? 0
101+
let modificationDate = attributes[.modificationDate] as? Date ?? Date()
102+
let formatter = DateFormatter()
103+
formatter.dateStyle = .medium
104+
formatter.timeStyle = .short
105+
fileAttributes = "Size: \(ByteCountFormatter.string(fromByteCount: fileSize, countStyle: .file)), Modified: \(formatter.string(from: modificationDate))"
106+
} catch {
107+
fileAttributes = "File in Downloads folder"
108+
}
109+
110+
return FileResource(
111+
uri: fileURL,
112+
name: fileURL.lastPathComponent,
113+
description: fileAttributes
114+
)
115+
}
116+
} catch {
117+
logToStderr("Error listing files in Downloads folder: \(error)")
118+
return []
119+
}
120+
}
121+
122+
/// Gets a resource by URI
123+
/// - Parameter uri: The URI of the resource to get
124+
/// - Returns: The resource content, or nil if the resource doesn't exist
125+
/// - Throws: MCPResourceError if there's an error getting the resource
126+
func getResource(uri: URL) throws -> MCPResourceContent? {
127+
// Check if the file exists
128+
guard FileManager.default.fileExists(atPath: uri.path) else {
129+
return nil
130+
}
131+
132+
// Get the resource content
133+
return try FileResourceContent.from(fileURL: uri)
134+
}
135+
136+
/// Function to log a message to stderr
137+
private func logToStderr(_ message: String) {
138+
fputs("\(message)\n", stderr)
139+
}
67140
}

0 commit comments

Comments
 (0)